前言
面试被问到了这个问题,回答的很糟糕,这里记录一下。
当时回答的是在读取时用while循环判断是否为空,是的话就sleep一段时间,再重新判断,不为空则读取并返回,在插入时用while循环判断是否为满,为满则sleep一段时间,再重新判断,不为满则插入。
完全没有考虑线程安全的问题,同样用循环等待的方式也太过暴力了,实在是一个糟糕的设计。
分析
我们知道阻塞队列相比于普通队列,区别在于当队列为空时获取被阻塞,当队列为满时插入被阻塞。当被阻塞时,我们的获取或插入线程进入阻塞状态,当队列中有了元素或者有剩余空间时,我们的线程继续执行,那么这个问题实际上有两个关键点,线程如何阻塞?满足条件后线程如何知道OK了并继续执行(如队列不为空后读取线程继续执行)?
对于第一个问题,阻塞有两种方法,wait和sleep。
对于第二个问题,这实际上是一个线程间通信的问题,循环重试的方式相当于主动去查询状态,那么有没有类似回调的方式,队列符合条件后告知我继续执行呢?当然有啦,就是notify。
思路
我们维护一个锁lock。
在读取的逻辑中,我们先判断队列是否为空,如果为空,我们则lock.wait使读取线程等待,如果不为空,我们则读取一个元素,并lock.notify,随机唤醒一个插入线程。
在插入的逻辑中,我们先判断队列是否为满,如果为满,我们则lock.wait使插入线程等待,如果不为满,我们则插入一个元素,并lock.notify,随机唤醒一个读取线程。
由于队列不可能同时为满又为空,因此等待的线程肯定为同一种线程(读取或者插入)。
我们维护一个cap容量,一个size队列大小,并使用一个大小为cap的数组来保存队列的值,使用head和tail两个指针来记录头尾位置。并将以上变量声明为volatile来保证线程安全。
我们将插入和读取加锁,保证同一时刻只有一个线程进行读写操作,防止重复读取或超出容量的插入。
代码
class BlockQueue<T> {
Object[] queue;
private volatile int head, tail, size;
private final int cap;
private final Object lock = new Object();
public BlockQueue(int cap) {
this.cap = cap;
queue = new Object[cap];
}
public void offer(T t) throws InterruptedException{
synchronized (lock) {
if(size == cap) {
lock.wait();
}
queue[tail++] = t;
tail %= cap;
size++;
lock.notify();
}
}
public T poll() throws InterruptedException {
T res;
synchronized (lock) {
if(size == 0) {
lock.wait();
}
res = (T)queue[head++];
head %= cap;
size--;
lock.notify();
}
return res;
}
}
测试代码
public class Test {
public static void main(String[] args){
BlockQueue<Integer> blockQueue = new BlockQueue<>(10);
// 搞两个线程, 分别模拟生产者和消费者.
// 第一次, 让给消费者消费的快一些, 生产者生产的慢一些.
// 此时就预期看到, 消费者线程会阻塞等待. 每次有新生产的元素的时候, 消费者才能消费
// 第二次, 让消费者消费的慢一些, 生产者生产的快一些.
// 此时就预期看到, 生产者线程刚开始的时候会快速的往队列中插入元素, 插入满了之后就会阻塞等待.
// 随后消费者线程每次消费一个元素, 生产者才能生产新的元素.
Thread producer = new Thread(){
@Override
public void run() {
for(int i = 0; i<1000; i++) {
try {
blockQueue.offer(i);
System.out.println("生产元素:"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
producer.start();
Thread consumer = new Thread() {
@Override
public void run() {
while (true) {
try {
int ret = blockQueue.poll();
System.out.println("消费元素: " + ret);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
consumer.start();
}
}
当消费慢于生产时(队列容量为10)
消费元素: 0
生产元素:0
生产元素:1
生产元素:2
生产元素:3
生产元素:4
生产元素:5
生产元素:6
生产元素:7
生产元素:8
生产元素:9
生产元素:10
消费元素: 1
生产元素:11
生产元素:12
消费元素: 2
消费元素: 3
生产元素:13
消费元素: 4
生产元素:14
消费元素: 5
生产元素:15
消费元素: 6
生产元素:16
消费元素: 7
生产元素:17
消费元素: 8
当生产慢于消费时
生产元素:0
消费元素: 0
生产元素:1
消费元素: 1
生产元素:2
消费元素: 2
生产元素:3
消费元素: 3
生产元素:4
消费元素: 4
生产元素:5
消费元素: 5
生产元素:6
消费元素: 6
消费元素: 7
生产元素:7
生产元素:8
消费元素: 8
生产元素:9
消费元素: 9
生产元素:10
消费元素: 10
生产元素:11
消费元素: 11
生产元素:12
消费元素: 12
消费元素: 13
生产元素:13
均符合预期。