一、简介

什么是阻塞队列?我们都知道队列具有先进先出的特点,那么有阻塞特性(即队列满了阻塞生产者,队列空了阻塞消费者)的队列,称为阻塞队列

阻塞队列被广泛应用于生产者-消费者模式中,在实际开发中,我们经常使用LinkedBlockingQueue来作为阻塞队列,而为什么使用LinkedBlockingQueue而不使用ArrayBlockingQueue,下面会通过对LinkedBlockingQueue的源码解读来解答这个问题。

二、LinkedBlockingQueue的基本使用

2.1 基本结构

LinkedBlockingQueue是基于单向链表的队列,结构图如下
截图.png

2.2 常用api

BlockingQueue接口提供了许多方法,常用的如下:

方法\处理方式抛出异常返回特殊值一直阻塞超时退出
插入add(e)offer(e)put(e)put(e, time, unit)
移除remove(e)poll()take()poll(time, unit)
检查element()peek()--

可以看到,插入、移除元素的时候都各有4种方法,每种方法有对应的处理方式(抛出异常、返回特殊值、一直阻塞、超时退出

2.3 简单示例

下面是一个模拟生产者-消费者的简单示例,生产者线程定时向阻塞队列中插入消息,消费者线程不断从阻塞队列中消费消息

public class LinkedBlockingQueueMain {
    private final static BlockingQueue<String> queue = new LinkedBlockingQueue<>();

    public static void main(String[] args) {
        new Thread(new Producer()).start();
        new Thread(new Consumer()).start();
    }

    static class Producer implements Runnable {

        @Override
        public void run() {
            int count = 0;
            while (true) {
                try {
                    queue.put("message" + count++);
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class Consumer implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    String msg = queue.take();
                    System.out.println("消费到消息:" + msg);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

三、源码解读

3.1 关键属性

  • int capacity:队列容量
  • AtomicInteger count:当前队列所包含的元素个数
  • Node<E> head:头节点
  • Node<E> last:尾节点
  • ReentrantLock takeLocktake锁
  • Condition notEmpty队列不空(条件)
  • ReentrantLock putLockput锁
  • Condition notFull队列不满(条件)

可以看到这里有两把锁,一把是移除元素用的take锁,另一把是插入元素用的put锁,这是与ArrayBlockingQueue的重要区别

3.2 插入元素

这里介绍插入元素的put方法,如果队列满了,则阻塞生产者,直到队列中有足够的位置来存放元素。大致步骤如下:

  1. putLock加锁
  2. 判断队列元素个数是否已经达到容量大小,如果是则调用notFull条件的await方法,使当前生产者线程进入等待状态,直到被唤醒。否则直接下一步
  3. 将当前元素插入队尾
  4. 判断队列如果还没满,则会唤醒在等待notFull条件的某个生产者线程
  5. putLock解锁
  6. 如果原本队列为空,则唤醒在等待notEmpty条件的某个消费者线程

源码如下:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();

    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    
    // putLock加锁
    putLock.lockInterruptibly();
    try {
        // 判断队列元素个数是否已经达到容量大小,是则调用notFull条件的await方法,使当前生产者线程进入等待状态
        while (count.get() == capacity) {
            notFull.await();
        }
        // 当还有空闲位置或当前线程被唤醒,将当前元素插入队尾
        enqueue(node);
        c = count.getAndIncrement();
        // 判断队列如果还没满,则会唤醒在等待notFull条件的某个生产者线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        // putLock解锁
        putLock.unlock();
    }
    // 如果c等于0,说明队列中原本没有元素,则唤醒在等待notEmpty条件的某个消费者线程
    if (c == 0)
        signalNotEmpty();
}

3.3 移除元素

这里介绍移除元素的take方法,如果队列为空,则阻塞消费者,直到队列中有元素可供消费。大致步骤如下:

  1. takeLock加锁
  2. 判断队列是否为空,如果是则调用notEmpty条件的await方法,使当前消费者线程进入等待状态,直到被唤醒。否则直接下一步
  3. 移出队头元素
  4. 判断队列如果还有元素,则会唤醒在等待notEmpty条件的某个消费者线程
  5. takeLock解锁
  6. 如果原本队列是满的,则唤醒在等待notFull条件的某个生产者线程

源码如下:

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    // takeLock加锁
    takeLock.lockInterruptibly();
    try {
        // 当队列为空,则调用notEmpty条件的await方法,使当前消费者线程进入等待状态
        while (count.get() == 0) {
            notEmpty.await();
        }
        // 当队列元素不为空或当前消费者线程被唤醒,将队头元素出队
        x = dequeue();
        c = count.getAndDecrement();
        // 如果移除队头元素后队列中还有其他元素,则会唤醒在等待notEmpty条件的某个消费者线程
        if (c > 1)
            notEmpty.signal();
    } finally {
        // takeLock解锁
        takeLock.unlock();
    }
    // 如果原来队列是满的,则唤醒在等待notFull条件的某个生产者线程
    if (c == capacity)
        signalNotFull();
    return x;
}

四、总结

阻塞队列在并发编程中应用非常广泛,常用于生产者-消费者模式,在队列满或者空的情况下,可以阻塞生产者或者消费者,直到队列不满或者不空。

实际开发过程中一般使用LinkedBlockingQueue而不是ArrayBlockingQueue,由于其内部拥有两把锁putLock和takeLock,意味着在生产者在生产数据的同时,消费者可以消费数据,而ArrayBlockingQueue内部只有一把锁,生产者在生产数据的同时,消费者无法消费数据。


kamier
1.5k 声望493 粉丝