线程池的核心——BlockingQueue

本文详细介绍了 BlockingQueue 的概念,它是 Java 线程池的关键组件。从 Queue 接口开始,讨论了 BlockingQueue 的特点,如阻塞操作。接着,文章深入分析了 BlockingQueue 的几种实现类,包括 LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue 和 TransferQueue,探讨了它们各自的特点和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

本篇文章会介绍BlockingQueue的概念。

BlockingQueue是java线程池的核心。无论是在面试还是工作中,都是必知必会的知识点。

BlockingQueue的名字就完美概括了这个接口的特点,Queue 是BlockingQueue的父接口,说明它本质还是一个队列。Blocking意为阻塞,说明BlockingQueue相对于普通队列,新增了阻塞的特性。

本文会从BlockingQueue的父接口Queue开始聊起,包括Queue在java 容器中的定位以及API等等。然后介绍BlockingQueue相比于Queue新增出来的方法。最后会介绍几个BlockingQueue接口的具体实现类的特点和具体用法。

从父接口Queue开始聊起

Queue接口是Java容器大家庭中的一员,具体的定位如下图所示:

在这里插入图片描述
Queue自从java 1.5之后,成为接口Collection下的一员,Queue与ListSet最大的区别就在于,Queue是专门为高并发存在的。

除去基本的Collection自带的方法,Queue额外提供了增、删、查的方法。每一种操作又会有两种具体的实现方法,区别在于操作失败时,是抛出错误还是返回某个特定值。

下面对这些方法做一个总结

抛出异常返回特定值
add()offer() – 返回false
remove()poll() – 返回null
element()peek() – 返回null

注意,其中的查操作和删操作,只能操作队列的头节点,并不能对队列中间的节点执行操作。

BlockingQueue接口及具体实现类

BlockingQueue接口API

由于BlockingQueue是Queue的子接口,所以上面的方法都包含在BlockingQueue中。

而BlockingQueue与Queue的主要区别,在于它新增了阻塞方法

什么是阻塞方法呢?简而言之,是当这个操作暂时没法完成的时候,会阻塞住当前线程,直到这个操作完成或者超时。

比如向队列中新增一个元素,如果队列已经满了,这个新增操作就会阻塞住线程,接下来根据选择的具体方法的不同,要么会一直等到队列中有了空位,要么会等超时后返回某个特定值。

BlockingQueue,相对于Queue,新增了两种增加元素和删除元素的阻塞方法,而这些阻塞方法也分成两种类型,一种是一直等待,一种是超时返回特定值。

抛出异常返回特定值阻塞等待超时返回
add()offer() – 返回falseput()offer(e, time, unit) – 返回false
remove()poll() – 返回nulltake() poll(time, unit) – 返回null
element()peek() – 返回null不存在不存在

表中没有标红的方法是继承自Queue接口中的方法,而标红的则是BlockingQueue新增的方法。

具体实现类

BlockingQueue本身也只是个接口,真正发挥作用的是它的具体实现类。

而它的实现类还可以进一步的分类:

如果队列中的元素是按照先进先出排序的,那么可以用LinkedBlockingQueueArrayBlockingQueue

如果要自己实现排序原则,就需要PriorityBlockingQueueDelayQueue

如果要实现添加元素的线程继续追踪该元素直到该元素被取出,就需要SynchronousQueueTransferQueue

LinkedBlockingQueue 和 ArrayBlockingQueue

这两种具体实现的区别就在于底层的数据结构,一个使用链表而另一个使用数组。

从而也就导致了容量上的不同,理论上LinkedBlockingQueue的容量理论上是无限的,而ArrayBlockingQueue的容量受底层的数组限制。

从两者的构造函数就能看出上述区别,ArrayBlockingQueue的构造函数要求必须声明容量上限,而LinkedBlockingQueue可以不声明上限,默认是Integer.MAX_VALUE的值。

PriorityBlockingQueue 和 DelayQueue

Queue本身是先进先出的队列,各个元素在队列里的顺序是按照加入到容器的先后顺序。

但是PriorityBlockingQueue和DelayQueue就可以自定义元素在队列中的排序原则。

PriorityBlockingQueue默认使用元素的compareTo方法,按从小到大的顺序排列元素。调用take()会获取到队列中最小的元素。

也可以在调用构造函数时声明自定义的Comparator,队列中的元素会按照Comparator指定的规则进行排序。

DelayQueue要求队列中的所有的元素都实现Delayed接口,这些元素必须实现的方法是getDelay(TimeUnit unit)compareTo()方法。

元素在队列中按照compareTo指定的顺序排列。但是,将队列元素取出的操作还依赖于这个元素是否已经过期。这里过期的含义是getDelay()方法返回0或者负数。

比如下面这段代码:

public class BlockingQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        long currentTime = System.currentTimeMillis();
        DelayedTest a = new DelayedTest(currentTime + 1000, "a");
        DelayedTest b = new DelayedTest(currentTime + 200, "b");
        DelayedTest c = new DelayedTest(currentTime + 3000, "c");
        DelayedTest d = new DelayedTest(currentTime + 400, "d");
        DelayQueue<DelayedTest> queue = new DelayQueue<>();

        queue.put(a);
        queue.put(b);
        queue.put(c);
        queue.put(d);

        for (int i = 0; i < 4; i++) {
            System.out.println(queue.take());
        }

    }
}

class DelayedTest implements Delayed {
    final private long wait;
    final private String name;

    public DelayedTest(long wait, String name) {
        this.wait = wait;
        this.name = name;
    }

    @Override
    public int compareTo(Delayed o) {
        if (o.getDelay(TimeUnit.MILLISECONDS) < this.getDelay(TimeUnit.MILLISECONDS)) {
            return 1;
        } else if (o.getDelay(TimeUnit.MILLISECONDS) > this.getDelay(TimeUnit.MILLISECONDS)) {
            return -1;
        } else {
            return 0;
        }
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.wait - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public String toString() {
        return "DelayedTest{" +
                "name='" + name + '\'' +
                '}';
    }
}

代码分析:

这段代码中用DelayedTest类实现了Delayed接口,也就是实现了两个方法getDelay()compareTo()

其中,getDelay方法是定义了当前的对象还有多长时间进入“过期”状态,compareTo方法定义了,越早过期的对象排在前面,晚过期的对象排在后面。

在main方法中创建了4个对象,以当前系统时间为起点,分别在1000ms, 200ms, 3000ms, 400ms后进入过期状态。

输出的时候可以看到,输出语句之间有时间间隔,最后一句是在运行3秒后执行的。由此可见,只有在元素进入过期状态,take()方法才会把元素从队列中取出。

下图是执行结果:

在这里插入图片描述

可以说,DelayedQueue是一种特殊的PriorityBlockingQueue,同时又增加了新的功能,就是元素未过期不能被取出队列。

SynchronousQueue 和 TransferQueue

这两个实现类是很有趣的。因为这两个类中,可以实现用取出方法来阻塞添加方法。

什么意思呢?相比于别的具体实现类,只要队列不满,一个线程如果成功把对象添加到队列后,就会去执行其他操作,不再管这个元素的后续结果。

在SynchronousQueue和TransferQueue中,就实现了添加元素的线程对该元素的追踪效果。当一个线程向这两种队列添加一个元素时,这个线程进入阻塞状态,直到有其他线程将这个元素从队列中取走,这个线程才会继续执行。

而这两种具体实现的区别在于,

SynchronousQueue的队列容量为零,可以理解为这个队列并不是真实存在的。也就是说,任何线程调用put()方法向这个队列中添加数据,都会被阻塞住,直到其他线程调用take()方法才会唤醒这个线程。 对take()方法也是如此,如果一个线程先调用了take()方法,也会被阻塞住,直到另外一个线程向队列中put()了一个元素。

TransferQueue容量并不为零,并且保留了非阻塞式的put()方法,允许其他线程将元素加入到队列中就去执行别的操作。也添加了阻塞式的transfer(element)方法,当一个线程调用transfer方法后,进入阻塞状态,直到这个元素被其他线程take走。

看下面这段对TransferQueue的测试代码:

public class BlockingQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        TransferQueue<Integer> queue = new LinkedTransferQueue();

        (new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while(true) {
                        TimeUnit.SECONDS.sleep(1);
                        System.out.println("取出了" + queue.take());
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "takeThread")).start();

        queue.put(1);
        System.out.println("调用put方法");

        queue.transfer(2);
        System.out.println("调用transfer方法");
    }
}

上面代码的输出如下:

在这里插入图片描述

代码分析:

代码中创建了一个元素类型是Integer的TransferQueue,分别调用put()方法和transfer()方法把1和2加入到队列中。

调用完put方法和transfer方法后,会执行一句打印语句。

此外还有一个线程takeThread,每隔一秒就会调用take()方法从队列中取出元素。

根据上述代码的输出可以得出下面结论:

在调用完put()方法后直接执行了put后的打印语句,然后执行了元素的取出操作,说明put方法没有阻塞。

transfer()方法的打印语句发生在取出元素之后,说明是取出元素之后,打印语句才得到执行。也就是说,线程执行完transfer()方法后,就进入了阻塞状态,直到添加的元素被取出后才重新被唤醒。

总结

本文从Queue出发,讲了BlockingQueue相对于Queue的提升。

然后介绍了六种BlockingQueue具体实现类的用法和特点。

BlockingQueue的主要应用场景是在线程池中,可以根据具体的业务场景,选择不同的BlockingQueue的具体实现类用在线程池中。

### 有序线程池的概念 有序线程池是一种特殊的线程池实现,其主要特点是按照任务提交的顺序执行任务。这意味着即使多个任务被并行提交到线程池中,它们也会严格按照先入先出(FIFO)的原则依次完成。这种特性对于某些需要保持数据一致性或逻辑连续性的场景非常有用。 在 Java 的 `java.util.concurrent` 包中,并未直接提供一种名为“有序线程池”的类,但可以通过合理配置现有的线程池来实现这一功能。例如,使用单一线程的线程池或者自定义阻塞队列配合特定的线程池实现[^1]。 --- ### 使用单线程线程池实现有序执行 Java 提供了一个简单的单线程线程池实现——`Executors.newSingleThreadExecutor()`。该方法返回一个只有一个工作线程的线程池,因此所有的任务都会按提交顺序串行执行。 以下是基于此方法的一个简单示例: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class OrderedThreadPoolExample { public static void main(String[] args) { ExecutorService executor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 5; i++) { final int taskId = i; executor.submit(() -> { System.out.println("Executing Task " + taskId); }); } executor.shutdown(); } } ``` 在这个例子中,所有任务会严格按提交顺序逐一执行[^1]。 --- ### 自定义线程池实现有序执行 如果希望保留多线程的优势,同时仍然保证任务的执行顺序,则可以考虑以下两种方式之一: #### 方法一:使用 `LinkedBlockingQueue` 和固定大小线程池 通过设置 `ThreadPoolExecutor` 并指定 `LinkedBlockingQueue` 作为任务队列,可以确保任务按照 FIFO 原则排队等待执行。这种方式允许我们控制线程数量的同时维持任务的顺序性。 代码示例如下: ```java import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class CustomOrderedThreadPool { public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // 核心线程数 4, // 最大线程数 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>() ); for (int i = 0; i < 8; i++) { final int taskId = i; executor.execute(() -> { try { Thread.sleep(1000); // 模拟耗时操作 System.out.println("Task " + taskId + " executed by thread " + Thread.currentThread().getName()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } executor.shutdown(); } } ``` 在此示例中,尽管存在多个线程,但由于任务队列遵循 FIFO 原则,任务仍能按提交顺序被执行。 #### 方法二:结合 Spring Boot 实现本地消息队列 Spring Boot 可以通过集成线程池的方式实现更复杂的业务需求。例如,在处理异步任务时,我们可以利用 `taskManager.pushTask(...)` 接口将任务推送到内部的消息队列中,并由专门的消费者线程负责逐条消费和执行这些任务[^5]。 下面是一个简化版的实现思路: ```java @Component public class TaskManager { private BlockingQueue<TaskParam> queue = new LinkedBlockingQueue<>(); @PostConstruct public void init() { new Thread(() -> { while (true) { try { TaskParam param = queue.take(); // 阻塞直到有新任务到达 processTask(param); } catch (Exception ex) { ex.printStackTrace(); } } }).start(); } public void pushTask(TaskParam taskParam) throws InterruptedException { queue.put(taskParam); } private void processTask(TaskParam param) { System.out.println("Processing task with params: " + param.toString()); } } class TaskParam implements Serializable { private Class<?> listenerClass; private Map<String, Object> data = new HashMap<>(); public TaskParam(Class<?> listenerClass) { this.listenerClass = listenerClass; } public void put(String key, Object value) { data.put(key, value); } @Override public String toString() { return "Listener: " + listenerClass.getName() + ", Data: " + data; } } ``` 上述代码展示了如何借助 Spring 容器管理线程资源,从而构建一个支持有序任务调度的小型框架[^5]。 --- ### 动态调整线程池参数的最佳实践 为了进一步提高系统的灵活性与性能表现,建议引入动态线程池机制。这通常涉及监控当前负载情况并实时修改诸如核心线程数、最大线程数等关键属性[^2]。具体做法可能依赖于第三方库(如 Alibaba 的 Hystrix 或 Apache Commons Pool),也可以自行开发一套适配器接口封装底层逻辑。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值