面试题:如何设计一个阻塞队列?

前言

面试被问到了这个问题,回答的很糟糕,这里记录一下。

当时回答的是在读取时用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

均符合预期。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值