深入浅出BlockingQueue(二)

LinkedBlockingQueue

LinkedBlockingQueue 是一个基于链表的线程安全的阻塞队列:

  • 可以在队列头部和尾部进行高效的插入和删除操作。
  • 当队列为空时,取操作会被阻塞,直到队列中有新的元素可用。当队列已满时,插入操作会被阻塞,直到队列有可用空间。
  • 可以在构造时指定最大容量。如果不指定,默认为 Integer.MAX_VALUE,这意味着队列的大小受限于可用内存。
java 复制代码
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();

/**
 * Head of linked list.
 * Invariant: head.item == null
 */
transient Node<E> head;

/**
 * Tail of linked list.
 * Invariant: last.next == null
 */
private transient Node<E> last;

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
  • count: 一个 AtomicInteger,表示队列中当前元素的数量。通过原子操作保证其线程安全。
  • head: 队列的头部节点。由于这是一个 FIFO 队列,所以元素总是从头部移除。头部节点的 item 字段始终为 null,它作为一个虚拟节点,用于帮助管理队列。
  • last: 队列的尾部节点。新元素总是插入到尾部。
  • takeLock 和 putLock: 这是 LinkedBlockingQueue 中的两把 ReentrantLock 锁。takeLock 用于控制取操作,putLock 用于控制放入操作。这样的设计使得放入和取出操作能够在一定程度上并行执行,从而提高队列的吞吐量。
  • notEmpty 和 notFull: 这是两个 Condition 变量,分别与 takeLock 和 putLock 相关联。当队列为空时,尝试从队列中取出元素的线程将会在 notEmpty 上等待。当新元素被放入队列时,这些等待的线程将会被唤醒。同样地,当队列已满时,尝试向队列中放入元素的线程将会在 notFull 上等待,等待队列有可用空间时被唤醒。

链表的 Node 节点的定义如下:

java 复制代码
static class Node<E> {
    E item;

    /**
     * One of:
     * - the real successor Node
     * - this Node, meaning the successor is head.next
     * - null, meaning there is no successor (this is the last node)
     */
    Node<E> next;

    Node(E x) { item = x; }
}

01)item: 这个字段用于存储节点包含的元素。

02)next: 这个字段表示节点在队列中的后继节点。这个字段有三个可能的值:

  • 后继节点的实际引用。
  • 此节点自身的引用,意味着后继节点是头节点的下一个节点。
  • null,表示没有后继节点,也就是说此节点是队列的最后一个节点。

03)Node(E x): 这是节点类的构造方法,它接受一个元素 x 并将其赋值给 item 字段。

1)put 方法详解

put 方法源码如下:

java 复制代码
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    // Note: convention in all put/take/etc is to preset local var
    // holding count negative to indicate failure unless set.
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        /*
         * Note that count is used in wait guard even though it is
         * not protected by lock. This works because count can
         * only decrease at this point (all other puts are shut
         * out by lock), and we (or some other waiting put) are
         * signalled if it ever changes from capacity. Similarly
         * for all other uses of count in other wait guards.
         */
		//如果队列已满,则阻塞当前线程,将其移入等待队列
        while (count.get() == capacity) {
            notFull.await();
        }
		//入队操作,插入数据
        enqueue(node);
        c = count.getAndIncrement();
		//若队列满足插入数据的条件,则通知被阻塞的生产者线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}

put 方法的逻辑基本上和 ArrayBlockingQueue 的一样。

01)参数检查:如果传入的元素为 null,则抛出 NullPointerException。LinkedBlockingQueue 不允许插入 null 元素。

02)局部变量初始化:

  • int c = -1; 用于存储操作前的队列元素数量,预设为 -1 表示失败,除非稍后设置。
  • Node node = new Node(e); 创建一个新的节点包含要插入的元素 e。
  • final ReentrantLock putLock = this.putLock; 和 final AtomicInteger count = this.count; 获取队列的锁和计数器对象。

03)获取锁:putLock.lockInterruptibly(); 尝试获取用于插入操作的锁,如果线程被中断,则抛出 InterruptedException。

04)等待队列非满:如果队列已满(count.get() == capacity),当前线程将被阻塞,并等待 notFull 条件被满足。一旦有空间可用,线程将被唤醒继续执行。

05)入队操作:调用 enqueue(node); 将新节点插入队列的尾部。

06)更新计数:通过 c = count.getAndIncrement(); 获取并递增队列的元素计数。

07)检查并可能的唤醒其他生产者线程:如果队列没有满(c + 1 < capacity),使用 notFull.signal(); 唤醒可能正在等待插入空间的其他生产者线程。

08)释放锁:finally 块确保锁在操作完成后被释放。

09)可能的唤醒消费者线程:如果插入操作将队列从空变为非空(c == 0),则调用 signalNotEmpty(); 唤醒可能正在等待非空队列的消费者线程。

2)take 方法详解

take 方法的源码如下:

java 复制代码
public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
		//当前队列为空,则阻塞当前线程,将其移入到等待队列中,直至满足条件
        while (count.get() == 0) {
            notEmpty.await();
        }
		//移除队头元素,获取数据
        x = dequeue();
        c = count.getAndDecrement();
        //如果当前满足移除元素的条件,则通知被阻塞的消费者线程
		if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

01)局部变量初始化:

  • E x; 用于存储被取出的元素。
  • int c = -1; 用于存储操作前的队列元素数量,预设为 -1 表示失败,除非稍后设置。
  • final AtomicInteger count = this.count; 和 final ReentrantLock takeLock = this.takeLock; 获取队列的计数器和锁对象。

02)获取锁:takeLock.lockInterruptibly(); 尝试获取用于取出操作的锁,如果线程被中断,则抛出 InterruptedException。

03)等待队列非空:如果队列为空(count.get() == 0),当前线程将被阻塞,并等待 notEmpty 条件被满足。一旦队列非空,线程将被唤醒继续执行。

04)出队操作:调用 x = dequeue(); 从队列的头部移除元素,并将其赋值给 x。

05)更新计数:通过 c = count.getAndDecrement(); 获取并递减队列的元素计数。

06)检查并可能的唤醒其他消费者线程:如果队列仍有其他元素(c > 1),使用 notEmpty.signal(); 唤醒可能正在等待非空队列的其他消费者线程。

07)释放锁:finally 块确保锁在操作完成后被释放。

08)可能的唤醒生产者线程:如果取出操作将队列从满变为未满(c == capacity),则调用 signalNotFull(); 唤醒可能正在等待插入空间的生产者线程。

09)返回取出的元素:最后返回被取出的元素 x。

3)使用示例

java 复制代码
public class LinkedBlockingQueueTest {
    private static LinkedBlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>(10);

    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() {
            for (int i = 0; i < 100; i++) {
                try {
                    blockingQueue.put(i);
                    System.out.println("生产者生产数据:" + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Integer data = blockingQueue.take();
                    System.out.println("消费者消费数据:" + data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

程序运行的部分结果如下图所示:

相关推荐
识君啊1 小时前
Java 栈 - 附LeetCode 经典题解
java·数据结构·leetcode·deque··stack·lifo
shehuiyuelaiyuehao1 小时前
关于hashset和hashmap,还有treeset和treemap,四个的关系
java·开发语言
马尔代夫哈哈哈1 小时前
Spring AOP
java·后端·spring
only-qi1 小时前
Java 包装器模式:告别“类爆炸“
java·开发语言
Yweir1 小时前
Java 接口测试框架 Restassured
java·开发语言
wangbing11251 小时前
开发指南141-类和字节数组转换
java·服务器·前端
~央千澈~1 小时前
抖音弹幕游戏开发之第15集:添加配置文件·优雅草云桧·卓伊凡
java·前端·python
肖。35487870941 小时前
html中onclick误区,后续变量会更改怎么办?
android·java·javascript·css·html
码云数智-园园1 小时前
Java Swing 界面美化与 JPanel 优化完全指南:从复古到现代的视觉革命
java·开发语言