阻塞队列是一种特殊的队列,它支持在队列满或者空时阻塞线程。在多线程编程中,阻塞队列是一种常用的同步工具,可以用来实现生产者-消费者模型等多线程协作场景。
1. 基本原理
阻塞队列是一种线程安全的队列,它支持在队列满或者空时阻塞线程。在阻塞队列中,当生产者向队列插入元素时,如果队列已满,则生产者线程会被阻塞,直到有消费者从队列中取出元素为止;当消费者从队列中取出元素时,如果队列为空,则消费者线程会被阻塞,直到有生产者向队列中插入元素为止。
阻塞队列的实现通常需要借助某种形式的锁来保证线程安全。在插入或者删除元素时,需要先获取锁,并在操作完成后释放锁。当队列已满或者空时,需要使用条件变量来等待或者唤醒相应的线程。
2. 使用方法
Java标准库提供了多种阻塞队列的实现,如ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。这些阻塞队列都实现了BlockingQueue接口,提供了一系列阻塞队列的操作方法,如put()、take()、offer()、poll()等。
以ArrayBlockingQueue为例,我们可以使用以下代码来创建一个容量为10的阻塞队列:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
然后可以使用put()方法向队列中插入元素,使用take()方法从队列中取出元素:
queue.put(1); // 队列满时会阻塞线程
int value = queue.take(); // 队列空时会阻塞线程
除了put()和take()方法之外,阻塞队列还提供了其他一些方法,如offer()、poll()、offer(long timeout, TimeUnit unit)、poll(long timeout, TimeUnit unit)等。这些方法可以在队列已满或者空时分别返回false或者null,或者等待一段时间后再返回。
注意:offer和put都是入对列,但是put具有阻塞功能,同样的 poll 和 take 都是出队列,但是 take 具有阻塞功能。
3. 常见实现
Java标准库提供了多种阻塞队列的实现,下面介绍其中几种常见的实现:
- ArrayBlockingQueue:基于数组实现的有界阻塞队列。它按照先进先出的原则对元素进行排序。
- LinkedBlockingQueue:基于链表实现的有界(默认为Integer.MAX_VALUE)或无界阻塞队列。它按照先进先出的原则对元素进行排序。
- SynchronousQueue:不存储元素的阻塞队列。每个插入操作必须等待一个相应的删除操作,反之亦然。
- PriorityBlockingQueue:基于优先级堆实现的无界阻塞队列。它可以根据元素的优先级进行排序。
4. 使用场景和好处
-
解耦与解决生产者-消费者问题:阻塞队列可以将生产者和消费者解耦,使得它们可以独立地进行操作。生产者可以向队列中插入元素,而消费者可以从队列中取出元素,它们不需要直接通信,而是通过队列来进行协作。这种方式简化了多线程编程的复杂性,提高了代码的可读性和可维护性。
-
避免空转和忙等:阻塞队列可以避免线程的空转和忙等。当队列为空时,消费者线程可以被阻塞,不会一直占用CPU资源,直到有新的元素插入;当队列已满时,生产者线程可以被阻塞,不会一直尝试插入元素,浪费CPU资源。这种通过阻塞来等待条件满足的方式可以提高系统的效率和资源利用率。
-
控制系统的吞吐量:通过调整阻塞队列的容量,可以控制系统的吞吐量。当队列容量较大时,生产者和消费者可以以较快的速度进行操作,从而提高系统的吞吐量;当队列容量较小时,生产者和消费者之间的速度会被限制,从而保证系统不会过载。
5. 简单实现一个阻塞队列
5.1 实现一个普通队列
这里实现一个环形队列为例:
class MyBlockingQueue {
private String[] elem = null;
private int head = 0;//队头位置
private int tail = 0;//队尾位置
private int size = 0; //当前元素数量
private int capability = 10; //队列的容量,默认为10
public MyBlockingQueue() {
elem = new String[capability];
}
public MyBlockingQueue(int capability) {
this.capability = capability;
elem = new String[capability];
}
public void put(String s) {
if(size == capability) {
//队列满
return;
}
elem[tail] = s;
size++;
if(++tail == capability) {
tail = 0;
}
}
public String take() {
if(size == 0) {
//队列空
return null;
}
String ret = elem[head];
size--;
if(++head == capability) {
head = 0;
}
return ret;
}
}
我们发现上述代码有很多写操作,如果有多个线程使用同一个队列,是有可能出现线程安全问题的,所以我们需要加锁:
class MyBlockingQueue {
private String[] elem = null;
private int head = 0;//队头元素
private int tail = 0;//队尾元素
private int size = 0; //当前元素数量
private int capability = 10; //队列的容量,默认为10
public MyBlockingQueue() {
elem = new String[capability];
}
public MyBlockingQueue(int capability) {
this.capability = capability;
elem = new String[capability];
}
public void put(String s) {
//这里相当于用调用这个方法的对象加锁
synchronized (this) {
if (size == capability) {
//队列满
return;
}
elem[tail] = s;
size++;
if (++tail == capability) {
tail = 0;
}
}
}
public String take() {
String ret = null;
synchronized (this) {
if (size == 0) {
//队列空
return null;
}
ret = elem[head];
size--;
if (++head == capability) {
head = 0;
}
notify();//唤醒put中的wait()
}
return ret;
}
}
5.2 对put和take方法进行改造
现在我们需要改进put和take方法,使之具有阻塞效果:
对于put来说,我们需要它在队列满时阻塞,直到队列不满为止
对于take来说,我们需要它在队列空时阻塞,直到队列不为空
于是我们可以使用 wait/notify 来改进,让,put 在队列满时调用wait 直到 调用take 出元素后 使用 notify 唤醒,同理,让 take 在队列空时 调用 wait 直到 put 入元素后 使用 notify 唤醒
class MyBlockingQueue {
private String[] elem = null;
private int head = 0;//队头元素
private int tail = 0;//队尾元素
private int size = 0; //当前元素数量
private int capability = 10; //队列的容量,默认为10
public MyBlockingQueue() {
elem = new String[capability];
}
public MyBlockingQueue(int capability) {
this.capability = capability;
elem = new String[capability];
}
public void put(String s) {
synchronized (this) {
if (size == capability) {
//队列满
try {
wait();//等出队列中的notify唤醒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
elem[tail] = s;
size++;
if (++tail == capability) {
tail = 0;
}
notify();
}
}
public String take() {
String ret = null;
synchronized (this) {
if (size == 0) {
//队列空
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
ret = elem[head];
size--;
if (++head == capability) {
head = 0;
}
notify();//唤醒put中的wait()
}
return ret;
}
}
现在我们的 put 和 take 方法就具有阻塞 功能了,但其实上述代码仍然存在一些问题:
问题1:当 put 或者 take 中的 wait 被唤醒后,它们需要参与锁竞争才能拿到锁继续执行,如果在这期间,其他线程先拿到锁执行了 put 或者 take ,导致 队列又满了或者又空了,但是我们的代码中已经执行过了 if 就会认为 队列 没有满或者空而继续执行,于是会导致数组越界问题。
问题2:如果有两个 put 在等待唤醒,此时执行了一个 take,唤醒了一个 put ,那么这个put 中的 notify就会唤醒另一个put但此时队列又满了,同理,如果有两个 take,在等待唤醒,此时执行了一个put ,这个put唤醒的take 会唤醒另一个 take 但此时 队列又空了。
所以我们要保证,put 和 take 每次被唤醒后拿到锁 都要进行一次判断,所以我们把if改为while即可解决这个问题:
class MyBlockingQueue {
private String[] elem = null;
private int head = 0;//队头元素
private int tail = 0;//队尾元素
private int size = 0; //当前元素数量
private int capability = 10; //队列的容量,默认为10
public MyBlockingQueue() {
elem = new String[capability];
}
public MyBlockingQueue(int capability) {
this.capability = capability;
elem = new String[capability];
}
public void put(String s) {
synchronized (this) {
while (size == capability) {
//队列满
try {
wait();//等出队列中的notify唤醒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
elem[tail] = s;
size++;
if (++tail == capability) {
tail = 0;
}
notify();
}
}
public String take() {
String ret = null;
synchronized (this) {
while (size == 0) {
//队列空
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
ret = elem[head];
size--;
if (++head == capability) {
head = 0;
}
notify();//唤醒put中的wait()
}
return ret;
}
}
6. 生产者消费者问题
生产者-消费者问题是一个经典的并发编程问题,涉及到多个线程之间的协作与同步。它描述了生产者线程和消费者线程共享一个有限缓冲区(也称为缓冲队列)的情景,生产者线程将数据放入缓冲区,而消费者线程从缓冲区中取出数据进行消费。
生产者-消费者问题的核心是实现生产者线程和消费者线程之间的合作与同步,确保以下两个条件得到满足:
- 缓冲区非空时,消费者线程可以从缓冲区中取出数据进行消费。
- 缓冲区未满时,生产者线程可以将数据放入缓冲区。
该问题的关键在于如何有效地实现线程间的通信和数据共享,以避免产生竞态条件和死锁等并发问题。
下面我们用刚才实现的阻塞队列做一个简单的示例:
public class ThreadDemo24 {
public static void main(String[] args) {
MyBlockingQueue q = new MyBlockingQueue(4);
//生产者
Thread t1 = new Thread(() -> {
for(int i = 0; true; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
q.put(Integer.toString(i));
System.out.println("生产了元素:" + i);
}
});
//消费者
Thread t2 = new Thread(() -> {
while(true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("消费了元素:" + q.take());
}
});
t1.start();
t2.start();
}
}