LinkedBlockingQueue 源码


LinkedBlockingQueue 内部结构采用链式存储,元素节点 Node,封装对象E泛型,Node 中 next 指向下一个节点。

static class Node {
    E item;
    Node next;
    Node(E x) { item = x; }
}

LinkedBlockingQueue 结构图。

阻塞队列LinkedBlockingQueue链式存储结构
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node(null);
}

capacity,容量,默认 Integer.MAX_VALUE。
count,当前队列元素数量,AtomicInteger 线程同步,可根据 count 与 capactiy 比较值作为是否已满的依据。
head/last 指针,初始化创建第一个 Node 节点作为头结点与尾节点,封装内容是null。

一、put() / offer() 方法

向尾部插入元素,加锁 putLock。区别是,如果队列已满,put() 方法线程阻塞,当有空余位置时,被唤醒,继续插入,offer() 方法直接返回插入失败。

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    Node node = new Node(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();//以下代码段加锁
    try {
        while (count.get() == capacity) {
            notFull.await();
        }
        enqueue(node); // 到这里插入,说明有空余至少一个。
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();//若还有至少一个空余,通知其他等待的不满信号。
    } finally {
        putLock.unlock();//解锁
    }
    if (c == 0)
        signalNotEmpty();
}

插入对象不能是空,封装E对象 Node 节点。
线程并发,一定要 ReentrantLock 加锁,put 锁,确保其他线程此刻无法访问上锁的代码段。
判断 count,AtomicInteger 类型,保持并发同步,若达到最大容量capacity,线程将阻塞,释放锁,在条件 notFull 等待,即等待不满信号唤醒。
若未达到最大容量,在队列尾部 enqueue 插入 Node 节点,count 自增,返回c仍是自增前的值,若+1还小于 capacity,说明此次插入后还有空余空间,发出未满通知信号。
这里为什么会发出一个 notFull 未满信号呢?
线程 A1 执行 put() 方法时,获取 putLock 锁,其他线程无法获取该锁,当 A1 线程发现队列已满,在 notFull 上等待,释放 putLock 锁,线程 A2 获取该锁,同样在 notFull 上等待,此刻,存在多个阻塞的插入线程 A1.2.3... 都在 notFull 的 await 导致阻塞,释放 putLock 锁,在 notFull 上等待。
直到 take()/poll() 方法获取数据,队列出现空闲,signalNotFull 执行 notFull#signal 方法,在 notFull 对象等待队列上有线程在等待,唤醒 A1,signal 只唤醒 notFull 上的一个线程。A1 抢到 putLock 锁,执行元素插入,此刻其他线程仍休眠。enqueue 插入成功,再次判断是否有多余容量,若有,notFull#signal继续唤醒在 notFull 上休眠的其他线程。若没有notFull上的等待线程,执行 signal也没有影响,重点在于,它会继续唤醒其他 notFull 等待的线程。
最后,ReentrantLock 释放锁,如果c是0,说明插入之前队列是空,插入后不空,一定要发出不空信号 NotEmpty,唤醒阻塞在 NotEmpty 上的读取线程。

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    if (count.get() == capacity)
        return false;
    int c = -1;
    Node node = new Node(e);
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        if (count.get() < capacity) {
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
    return c >= 0;
}

判断 count 未达到 capactiy 最大容量,插入节点,同理,根据空余空间发出未满信号。c的初始值是-1,成功插入,则一定会赋值成一个 >=0 的值,因此,返回值可判断是否成功插入,offer() 方法不会阻塞。

二、take() / poll() 方法

从队列头部加锁 takeLock 获取元素,若队列为空,take()方法线程阻塞,有元素时唤醒。poll() 方法返回失败。

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;
}

逻辑和 put() 方法一样。

总结

put() 和 take() 是阻塞方法

offer() 和 poll() 是非阻塞方法。

生产者与消费者模型,在头部获取,尾部插入元素,并发时,采用两种锁保证线程同步,互相独立,并行操作队列元素,通过 AtomicInteger 数量更新同步。


任重而道远

你可能感兴趣的:(LinkedBlockingQueue 源码)