JAVA LinkedBlockingQueue详细分析

故事背景

最近面试被人问道了如何实现一个LinkedBlockingQueue,顿时懵逼。
对话如下:

面试官: 先介绍下你最近做过的项目吧.
我: 阿巴阿巴阿巴.
面试官: 好的,那么如果让你设计一个LinkedBlockingQueue,如何实现?
我: 好的,我会…emmm,忘记了(非常尴尬)

这东西堵塞队列我知道,但内部如何实现的还真不知道,因为类似的功能通常都考虑以异步的生产者消费者方式去实现(至少我是如此的)。
阻塞队列主要是以队列为中心,控制生产和消费速率,这对于异步队列就需要其他方式去控制。
希望借此文帮助学习分享。

结构分析

在这里插入图片描述
上图所示,LinkedBlockingQueue就是在集合的功能上又增加了队列的一些操作,开始逐个分析.

java.util.Collection

java集合的根接口,主要规定了集合的基本操作,包括单/多个元素的增删查,集合数量查询,流失对象返回.
作为根接口,为了保证通用性,其约束比较宽松,只是通过文档提出了对实现的规范.

java.util.AbstractCollection

public abstract class AbstractCollection<E> implements Collection<E>
在这里插入图片描述

这是对java.util.Collection接口的实现的基本骨架,是一个不可修改集合元素的实现类.
其中的方法在具体实现类中有更有效的处理方式的话可以替换.
实现方法依赖java.util.Iterator,如果要一个可修改的集合需要在子类重新实现取代默认的迭代器.

java.util.Queue

public interface Queue<E> extends Collection<E>
在这里插入图片描述

这是队列的根接口,主要在java.util.Collection接口的基础上,额外提供了不抛出异常的增取看操作.
此外其元素的存储应该是有序的,但具体顺序有由子类处理.
堵塞队列交由java.util.concurrent.BlockingQueue接口定义.
null由于会被当做队列当前没有元素的毒丸对象(特殊值),最好不允许插入.
由于存储的元素是有序的,因此需要考虑重新实现equalshashCode方法(当元素值相等,但hash不同时).

java.util.concurrent.BlockingQueue

public interface BlockingQueue<E> extends Queue<E>
在这里插入图片描述
该接口在Queue的基础上,提供了具有时间限制的增取操作.
BlockingQueue提供了4种形式的操作:

Throws exception Special value Blocks Times out
Insert add(e) offer(e) put(e) offer(e, time, unit)
Remove remove() poll() take() poll(time, unit)
Examine element() peek()
原本集合的某些方法在队列中使用需要注意,因为其内部基于迭代器,不能保证线程安全。
文档特地说明了,队列操作方法(除了大块的操作,如addAll)需要线程安全,但不限定实现的方式.
多线程操作同一个队列需要按顺序操作.

注意: 不能往该队列添加null值,因为其被用作poll操作失败(由于容量有限,且在指定时间内未完成)的返回值,表示队列当前没有元素.

扫描二维码关注公众号,回复: 16326173 查看本文章

java.util.AbstractQueue

public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E>
在这里插入图片描述
该抽象类提供了Queue实现的基本骨架.
主要是Queue接口的相关方法是抛出异常而不是Collections返回的特殊值(nullfalse). 同时,不允许插入null值.

java.util.concurrent.LinkedBlockingQueue

public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable
在这里插入图片描述
LinkedBlockingQueue是堵塞队列的一种实现,其内部以单链表维护元素.
链表的实现方式相对于数组,具有较高的吞吐量,但在并发环境下不太好预测性能.

LinkedBlockingQueue详细分析

属性

// 最大容量,默认为Integer.MAX_VALUE
private final int capacity;

// 当前元素数量
private final AtomicInteger count = new AtomicInteger();

// 队列头, 不变 head.item == null
transient Node<E> head;

// 队尾, 不变 last.item == null
private transient Node<E> last;

// take, poll等操作持有的锁,其实就是对队列读取的锁
private final ReentrantLock takeLock = new ReentrantLock();

// 正在等待取的线程队列
private final Condition notEmpty = takeLock.newCondition();

// put, offer等操作持有的锁,其实就是对队列写的锁
private final ReentrantLock putLock = new ReentrantLock();

// 正在等待写入的线程队列
private final Condition notFull = putLock.newCondition();

static class Node<E> {
    
    
    E item;

    // 只会是下面情况的一种: 1.真的后继节点; 2.null,没有后继节点,也就是最后的节点; 3.head节点;
    Node<E> next;

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

通过上面的说明,能了解到LinkedBlockingQueue本身的基础是单链表,通过ReentrantLocknewCondition()生成对应操作的等待队列,以此保证线程安全及数据有无的通知.

构造函数

public LinkedBlockingQueue() {
    
    
    this(Integer.MAX_VALUE);
}

// 限制队列容量,并初始化队列的 head 和 last 节点.
public LinkedBlockingQueue(int capacity) {
    
    
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

// LinkedBlockingQueue(int capacity)初始化,然后加写锁,将集合c一个个入队.
public LinkedBlockingQueue(Collection<? extends E> c) {
    
    
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // 写锁(以重入锁实现,对队尾的插入进行控制)
    try {
    
    
        int n = 0;
        for (E e : c) {
    
    
        	// null元素抛出异常
            if (e == null)
                throw new NullPointerException();
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e)); //将元素封装成Node,入队
            ++n;
        }
        count.set(n);
    } finally {
    
    
        putLock.unlock(); // 释放
    }
}

入队出队

// 入队,将节点插入到last之前
private void enqueue(Node<E> node) {
    
    
    // assert putLock.isHeldByCurrentThread();
    // assert last.next == null;
    last = last.next = node;
}

// 出队,将head的后继返回出来
private E dequeue() {
    
    
    // assert takeLock.isHeldByCurrentThread();
    // assert head.item == null;
    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;
}

加锁,释放锁

// 完全加锁,避免其他线程写入和读取
// 在我看来就是加读写锁
void fullyLock() {
    
    
    putLock.lock();
    takeLock.lock();
}

// 完全解锁
// 在我看来就是释放读写锁
void fullyUnlock() {
    
    
    takeLock.unlock();
    putLock.unlock();
}

加元素

// 除非线程被中断,否则堵塞到写入成功
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.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();
}

// 添加元素,直到超时失败或者中断异常
public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {
    
    

    if (e == null) throw new NullPointerException();
    long nanos = unit.toNanos(timeout);
    int c = -1;
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
    
    
        while (count.get() == capacity) {
    
    
            if (nanos <= 0)
                return false;
            // 等待队列未满信号 nanos长的时间
            // 返回的nanos是收到信号后剩余的时间
            // 因为存在并发写入的可能,所以需要自旋/锁保证(自旋通常更高效)
            nanos = notFull.awaitNanos(nanos);
        }
        enqueue(new Node<E>(e));
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
    
    
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
    return true;
}

// 添加元素,有空间且添加成功返回true,否则false
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<E> node = new Node<E>(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;
}

取元素

取元素的行为与加元素的类似,只不过写锁变读锁,条件notFull变为notEmpty.

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

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    
    
    E x = null;
    int c = -1;
    long nanos = unit.toNanos(timeout);
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
    
    
        while (count.get() == 0) {
    
    
            if (nanos <= 0)
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
    
    
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

public E poll() {
    
    
    final AtomicInteger count = this.count;
    if (count.get() == 0)
        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;
}

删元素

// 删除p节点,trail为p的前驱节点
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();
}

// 移除匹配的元素
public boolean remove(Object o) {
    
    
    if (o == null) return false;
    fullyLock();
    try {
    
    
    	// trail表示当前的尾巴节点(item为null)
    	// p表当前比较的节点
    	// head,last都是item为null的特殊节点
        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();
    }
}

继承自Collection的方法

这些方法的实现大同小异,就不细讲了.
主要就是在开始完全加锁fullyUnlock(),然后通过链表遍历,获取所需元素.

ReentrantLock

重入锁.
其内部通过state去记录当前线程加锁次数,以此实现重入.
然后释放也需要相同的次数,实现完全释放.
ReentrantLock,当A线程已经获得锁时,B线程必须等待A释放才能获取.
内部是基于AQS实现的类CLH队列维护的2个队列,分别是等待取锁等待条件(该队列中的节点收到通知会被加入等待取锁队列).

java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject

条件对象.
其就是LinkedBlockingQueueprivate final Condition notFull = putLock.newCondition();newCondition()所创建的实际对象.
内部通过java.util.concurrent.locks.AbstractQueuedSynchronizer.NodenextWaiter属性维护节点的排他/共享标识.
因此,每当从LinkedBlockingQueue获取一个元素,同时会检查元素数量是否有剩余,如有调用notEmpty.signal()方法,通知一个等待获取该条件的线程.
从等待条件队列出队的线程会被加入到双链表的待执行队列中,等待前驱执行完毕,再执行.

加锁逻辑

// 非公平锁
static final class NonfairSync extends Sync {
    
    
    final void lock() {
    
    
        if (compareAndSetState(0, 1))
        	// 加锁成功 这是加锁的简单逻辑,应该是对锁争用不激烈的优化
            setExclusiveOwnerThread(Thread.currentThread());
        else
        	// 加锁失败,进入尝试取锁阶段
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
    
    
        return nonfairTryAcquire(acquires);
    }
    
    // 检查并设置当前锁的持有线程, true表示成功持有锁
	final boolean nonfairTryAcquire(int acquires) {
    
    
      final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
    
    
        	// 对减少CAS的优化,类似于双检索的逻辑
            if (compareAndSetState(0, acquires)) {
    
    
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
    
    
        	// 当前线程已经持有锁,则增加重入计数
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

	// 来自 AQS 的方法
	public final void acquire(int arg) {
    
    
		// 调用上面的 tryAcquire 逻辑
        if (!tryAcquire(arg) &&
        	// addWaiter 创建一个排他的CLH节点
        	// acquireQueued将创建好的Node加入到CLH队列中,并且会检查是否还存在前驱节点,如果没有,则自身会尝试取锁
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
}

非公平锁与公平锁逻辑基本相同,只有在开始 tryAcquire 时会额外检查是否存在前驱节点,如果有则直接返回取锁失败。

等待取锁的线程的队列:等待执行的线程的队列

CLH

JUC(java.util.concurrent)是java并发工具包,基本由Doug Lea编写,像这样大神人物写出的代码,有相当的学习价值。

其实现了多线程同步的基本框架,内部通过一个CLH队列维护多线程对条件资源的获取。

CLH(Craig, Landin, and Hagersten locks): 是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

JDK 1.8中AQS的CLH是一个变种的实现,主要将原本使用的单链表改造为双链表,增加了对前驱节点的引用,用于检查前驱节点失效时剔除以及直接唤醒。

一些技巧

将类成员变量复制到方法本地变量上

Doug Lea写的JUC很多地方都将类成员变量复制到方法本地变量上,为什么要多此一举?
<<Performance of locally copied members ?>>
内容如下:

It’s a coding style made popular by Doug Lea.
It’s an extreme optimization that probably isn’t necessary;
you can expect the JIT to make the same optimizations.
(you can try to check the machine code yourself!)
Nevertheless, copying to locals produces the smallest bytecode, and for low-level code it’s nice to write code that’s a little closer to the machine.
Also, optimizations of finals (can cache even across volatile reads) could be better. John Rose is working on that.
For some algorithms in j.u.c, copying to a local is necessary for correctness.

大致意思:

这是Doug Lea带来的编码风格.
一种极端的优化方式,可能没啥用。你可以期望 JIT 也会执行相同的优化。
将类成员变量复制为方法本地变量可使得生成最小的字节码;也更容易编写靠近机器的底层代码。
在某些算法中(特别是JDK并发包中的算法),复制为本地变量,或使用final变量,对于保证并发正确性很有必要。

所以我们并不需要对这种代码模式过于担心。你做好并发/同步相关正确性编码,保证算法正确性就行,不需要去刻意模仿使用这种代码模式。

猜你喜欢

转载自blog.csdn.net/weixin_46080554/article/details/108820344