JUC 之队列 LinkedBlockingQueue 详解

JUC 之队列 LinkedBlockingQueue 详解

上一篇我们讲解了 ArrayBlockingQueue 的实现原理,该篇我们来学习 LinkedBlockingQueue 的源码实现,通过名称我们可以知道它是通过链表实现的,那么具体是单向链表还是双向链表呢?这个我们就要从源码中去发现了。下面我们就开始源码之旅吧!

使用方式同 ArrayBlockingQueue ,这里就给使用示例了,直接进入源码阶段。

核心成员介绍

// 链表的数据节点信息,通过 Node 的信息我们可知 LinkedBlockingQueue 是一个单项链表实现的阻塞队列
static class Node<E> {
    E item;// 真实数据信息
    Node<E> next;// 指向下一个节点的引用
    Node(E x) { item = x; }
}
// 队列容量限制
private final int capacity;
// 记录当前队列中的元素个数
private final AtomicInteger count = new AtomicInteger();
// 队列的头结点
transient Node<E> head;
// 队列的尾结点
private transient Node<E> last;
// 获取数据线程的同步锁(消费线程的同步锁)
private final ReentrantLock takeLock = new ReentrantLock();
// 消费线程条件信息
private final Condition notEmpty = takeLock.newCondition();
// 生产者线程同步锁
private final ReentrantLock putLock = new ReentrantLock();
// 生产者线程条件信息
private final Condition notFull = putLock.newCondition();

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);// 初始化头尾节点,伪节点不包含数据信息
}

通过上述源码信息我们发现 LinkedBlockingQueue 内部实现依赖两把 ReentrantLock 锁,一个 takeLock 锁用于控制消费线程并发从队列中获取数据的安全性(保证 head 引用操作的原子性);一个 putLock 锁用于控制生产者线程并发向队列中放数据的安全性(保证 last 引用操作的原子性)。

核心方法讲解

put 方法

通过源码可知 LinkedBlockingQueue 采用的是尾插法插入数据,为了保证 last 的原子性这里使用了 putLock 锁,通过 Condition 条件控制生产者的阻塞与唤醒行为。

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    Node<E> node = new Node<E>(e);// 创建一个 Node 节点
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;// 记录当前队列中的元素个数
    putLock.lockInterruptibly();// 获取写锁
    try {
        while (count.get() == capacity) {
            notFull.await();// 队列满了,阻塞等待
        }
        enqueue(node);// 添加到队列中
        c = count.getAndIncrement();// count 原子性加一,并返回 加一 之前的 值
        if (c + 1 < capacity)
            notFull.signal();// 说明队列还有空间,唤醒其它阻塞的生产者线程继续生产数据(只会唤醒一个)
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();// 唤醒消费线程
}

private void enqueue(Node<E> node) {
    last = last.next = node;// 单项链表操作
}
offer 方法

该方法在队列满时直接返回 false,不阻塞生产线程。LinkedBlockingQueue 还提供了一个带超时时间的offer(E e, long timeout, TimeUnit unit) 方法,这两个方法唯一的区别就在于 在队列满时是否等待一段时间,其它处理逻辑均一致。此处不在过多赘述,详细读者一看便懂。

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    if (count.get() == capacity)
        return false;// 队列满了直接返回 false
    int c = -1;
    Node<E> node = new Node<E>(e);// 创建一个 Node 节点
    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;
}
take 方法

获取数据,当队列为空时阻塞等待。通过 takeLock 锁 和 Condition实现。

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        // 此处为啥用 while 循环?上一步不是已经获取了 takeLock 锁吗?大家可以先想一下【注 1】
        while (count.get() == 0) {
            notEmpty.await();// 队列为空时阻塞等待
        }
        x = dequeue();// 获取数据
        c = count.getAndDecrement();// 计数原子性减一
        if (c > 1)
            notEmpty.signal();// 队列中还有数据,唤醒其它消费者线程(唤醒一个)
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)// 因为 c 在上一步已经减了 1 
        signalNotFull();// 队列未满,唤醒一个生产者
    return x;
}
dequeue 方法

出队操作,从头节点取,该方法是线程安全的,单链表操作

private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // help GC
    head = first;// 更新头结点
    E x = first.item;
    first.item = null;
    return x;
}
poll 方法

获取数据,与 take 方法的不同之处在于 队列为空时返回 null,不会阻塞消费者线程。而 poll(long timeout, TimeUnit unit) 方法提供了阻塞等待一定时间的功能,其它逻辑同 poll 方法。

public E poll() {
    final AtomicInteger count = this.count;
    if (count.get() == 0) // 队列为空时 返回null,如果 调用的是  poll(long timeout, TimeUnit unit) 该方法,队列为空时将阻塞等待给定的时间,等待时间结束,如果还是没有数据 返回 null
        return null;
    E x = null;
    int c = -1;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        if (count.get() > 0) {
            x = dequeue();// 获取数据
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();// 队列中还有数据,唤醒其它消费者线程(唤醒一个)
        }
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();// 队列未满,唤醒一个生产者
    return x;
}
remove 方法

删除指定元素,注意删除操作的时候获取了 读锁和写锁

public boolean remove(Object o) {
    if (o == null) return false;
    fullyLock();
    try {
        // 从头结点开始遍历查找,找到对应元素将其从链表中移除
        for (Node<E> trail = head, p = trail.next;
             p != null;
             trail = p, p = p.next) {
            if (o.equals(p.item)) {
                unlink(p, trail);// 从链表中移除数据
                return true;
            }
        }
        return false;
    } finally {
        fullyUnlock();
    }
}
// 单向链表操作
void unlink(Node<E> p, Node<E> trail) {
    p.item = null;
    trail.next = p.next;
    if (last == p)
        last = trail;
    if (count.getAndDecrement() == capacity)
        notFull.signal();// 唤醒 生产者
}
peek 方法

获取数据但是不会从队列中移除

public E peek() {
    if (count.get() == 0)
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        Node<E> first = head.next;
        if (first == null)
            return null;
        else
            return first.item;
    } finally {
        takeLock.unlock();
    }
}
drainTo 方法

从队列中取出指定数量的元素放入集合 c 中

public int drainTo(Collection<? super E> c, int maxElements) {
    // ... 删除了检测代码
    boolean signalNotFull = false;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        int n = Math.min(maxElements, count.get());
        Node<E> h = head;
        int i = 0;
        try {
            while (i < n) {
                Node<E> p = h.next;
                c.add(p.item);
                p.item = null;
                h.next = h;
                h = p;
                ++i;
            }
            return n;
        } finally {
            if (i > 0) {
                head = h;// 更新 头结点
                signalNotFull = (count.getAndAdd(-i) == capacity);// count 计数原子性 减 i ,两者相等表明之前队列是满的
            }
        }
    } finally {
        takeLock.unlock();
        if (signalNotFull)
            signalNotFull();// 唤醒一个生产者线程
    }
}
注 1:

还记得上述遗留的一个问题吗?take 方法中 为什么使用 while 循环判断 count 是否等于 0?在多线程并发的情况下,当生产者向队列中插入了一条数据,由于 count 使用的是 AtomicInteger 的原子类,而 Condition 的唤醒操作是将等待的线程节点先移动到锁竞争的竞争队列中,然后再去抢锁等待OS唤醒调度执行,但是在此唤醒状态过程中如果有其它线程先拿到锁把数据取走了,然后又将 count 值减为了 0,如果使用 if 条件将不会从新检测队列中的值,这时就有问题了。所以在这种场景下必须使用 while 循环来判断。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值