深入浅出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();
                }
            }
        }
    }
}

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

相关推荐
TAN-90°-3 小时前
Java 6——成员变量初始值 object equals和== toString instanceof 参数传递问题
java·开发语言
中新传媒3 小时前
德宸堂心理双师同诊
java·前端·数据库
想唱rap3 小时前
NAT、内网穿透、代理服务
java·linux·网络·网络协议·udp·智能路由器
环流_3 小时前
nacos环境隔离
java·服务器·前端
芋只因3 小时前
天机学堂学习笔记
java·笔记·学习
摇滚侠3 小时前
Spring 面试题 真正的 offer 偏方 Java 基础 Java 高级
java·后端·spring
凯瑟琳.奥古斯特3 小时前
IP组播跨子网传输核心技术解析
java·开发语言·网络·网络协议·职场和发展
若水不如远方3 小时前
Java JSON 序列化原理与实战问题总结
java
hexu_blog3 小时前
前端vue后端java+springboot如何实现pdf,word,excel之间的相互转换
java·前端·vue.js·spring boot·文档转换
贺国亚3 小时前
synchronized- 并发
java·面试