非阻塞队列ConcurrentLinkedQueue与阻塞队列LinkedBlockingQueue原理探究

本文深入探讨了ConcurrentLinkedQueue和LinkedBlockingQueue的工作原理,包括它们的内部结构、offer和poll操作流程,以及如何通过锁和条件变量实现线程间的同步。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载自:http://ifeve.com/%E5%B9%B6%E5%8F%91%E9%98%9F%E5%88%97-%E6%97%A0%E7%95%8C%E9%9D%9E%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97concurrentlinkedqueue%E5%8E%9F%E7%90%86%E6%8E%A2%E7%A9%B6/
              http://ifeve.com/%E5%B9%B6%E5%8F%91%E9%98%9F%E5%88%97-%E6%97%A0%E7%95%8C%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97linkedblockingqueue%E5%8E%9F%E7%90%86%E6%8E%A2%E7%A9%B6/


ConcurrentLinkedQueue

       ConcurrentLinkedQueue中有两个volatile类型的Node节点分别用来存放列表的首尾节点,其中head节点存放链表第一个item为null的节点,tail则并不是总指向最后一个节点。Node节点内部则维护一个变量item用来存放节点的值,next用来存放下一个节点,从而链接为一个单向无界列表。
    private transient volatile Node<E> head;

    private transient volatile Node<E> tail;

    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }
       空构造函数初始化时候会构建一个item为NULL的空节点作为链表的首尾节点。
       Node类的定义很特别,特地贴码一下:
    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;

        /**
         * Constructs a new node.  Uses relaxed write because item can
         * only be seen after publication via casNext.
         */
        Node(E item) {
            UNSAFE.putObject(this, itemOffset, item);
        }

        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }

        void lazySetNext(Node<E> val) {
            UNSAFE.putOrderedObject(this, nextOffset, val);
        }

        boolean casNext(Node<E> cmp, Node<E> val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long itemOffset;
        private static final long nextOffset;

        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> k = Node.class;
                itemOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("item"));
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }
offer操作
       offer操作是在链表末尾添加一个元素:
public boolean offer(E e) {
    checkNotNull(e);

    final Node<E> newNode = new Node<E>(e);

    //从尾节点插入
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        
        if (q == null) {
            //如果q=null说明p是尾节点则插入
            if (p.casNext(null, newNode)) { //(1)
                //cas成功说明新增节点已经被放入链表,然后设置当前尾节点
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        else if (p == q) //(2)
            //多线程操作时候,由于poll时候会把老的head变为自引用,然后head的next变为新head,所以这里需要
            //重新找新的head,因为新的head后面的节点才是有效的节点
            p = (t != (t = tail)) ? t : head;
        else
            // 寻找尾节点(3)
            p = (p != t && t != (t = tail)) ? t : q;
    }
}
       从构造函数知道一开始有个item为null的哨兵节点,并且head和tail都是指向这个节点,下面看下当一个线程调用offer时候的情况:
       
       如图首先查找尾节点,q==null,p就是尾节点,所以执行p.casNext通过cas设置p的next为新增节点,这时候p==t所以不重新设置尾节点为当前新节点。由于多线程可以调用offer方法,所以可能两个线程同时执行到了(1)进行cas,那么只有一个会成功(假如线程1成功了),成功后的链表为:
      
      失败的线程会循环一次这时候指针为:
       
       这时候会执行(3)所以p=q,然后在循环后指针位置为:
       
       所以没有其他线程干扰的情况下会执行(1)执行cas把新增节点插入到尾部,没有干扰的情况下线程2 cas会成功,然后去更新尾节点tail,由于p!=t所以更新。这时候链表和指针为:
       
poll操作
       poll操作是在链表头部获取并且移除一个元素:
public E poll() {
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;

            // 当前节点有值则cas变为null(1)
            if (item != null && p.casItem(item, null)) {
                //cas成功标志当前节点以及从链表中移除
                if (p != h) // 类似tail间隔2设置一次头节点(2)
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            //当前队列为空则返回null(3)
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            //自引用了,则重新找新的队列头节点(4)
            else if (p == q)
                continue restartFromHead;
            else //(5)
                p = q;
        }
    }
}
    final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p))
            h.lazySetNext(h);
    }
       当队列为空时候,可知执行(3)这时候有两种情况,第一没有其他线程添加元素时候,(3)结果为true,然后因为h!=p为false所以直接返回null。第二在执行q=p.next前,其他线程已经添加了一个元素到队列,这时候(3)返回false,然后执行(5)p=q,然后继续循环。

size操作
       获取当前队列元素个数,在并发环境下不是很有用,因为使用CAS没有加锁所以从调用size函数到返回结果期间有可能增删元素,导致统计的元素个数不精确。
public int size() {
    int count = 0;
    for (Node<E> p = first(); p != null; p = succ(p))
        if (p.item != null)
            // 最大返回Integer.MAX_VALUE
            if (++count == Integer.MAX_VALUE)
                break;
    return count;
}


LinkedBlockingQueue

       LinkedBlockingQueue中也有两个Node分别用来存放首尾节点,并且里面有个初始值为0的原子变量count用来记录队列元素个数,另外里面有两个ReentrantLock的独占锁,分别用来控制元素入队和出队加锁,其中takeLock用来控制同时只有一个线程可以从队列获取元素,其他线程必须等待,putLock控制同时只能有一个线程可以获取锁去添加元素,其他线程必须等待。另外notEmpty和notFull用来实现入队和出队的同步。
    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() {
        this(Integer.MAX_VALUE);
    }
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

offer操作
       介绍下带超时时间的offer方法:在队尾添加元素,如果队列满了,那么等待timeout时候,如果时间超时则返回false,如果在超时前队列有空余空间,则插入后返回true。
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;
            //否者调用await进行等待,超时则返回<=0(1)
            nanos = notFull.awaitNanos(nanos);
        }
        //await在超时时间内返回则添加元素(2)
        enqueue(new Node<E>(e));
        c = count.getAndIncrement();
        
        if (c + 1 < capacity) //队列不满则激活其他等待入队线程(3)
            notFull.signal();
    } finally {
        putLock.unlock();
    }

    // c==0说明队列里面有一个元素,这时候唤醒出队线程(4)
    if (c == 0)
        signalNotEmpty();
    return true;
}

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}
       如果获取锁前面线程调用了thread.interrupt(),并且后面没有调用interrupted()重置中断标志,调用lockInterruptibly时候会抛出InterruptedException异常。
       队列满的时候调用notFull.awaitNanos阻塞当前线程,当前线程会释放获取的锁,然后等待超时或者其他线程调用了notFull.signal()才会返回并重新获取锁,或者其他线程调用了该线程的interrupt方法设置了中断标志,这时候也会返回但是会抛出InterruptedException异常。
       如果超时则直接返回false,如果超时前调用了notFull.signal()则会退出循环,执行(2)添加元素到队列,然后执行(3),(3)的目的是为了激活其它入队等待线程。(4)的话c==0说明队列里面已经有一个元素了,这时候就可以激活等待出队线程了。
       另外signalNotEmpty函数是先获取独占锁,然后在调用的signal。这是因为无论是条件变量的await和singal都是需要先获取其对应的锁才能调用,因为条件变量使用的就是锁里面的state管理状态,否则会报异常。

poll操作
       获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回null:
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();
    }
    //当前队列容量为最大值-1则激活入队线程。
    if (c == capacity)
        signalNotFull();
    return x;
}
       首先获取独占锁,然后进入循环,当前队列有元素才会退出循环,或者超时了,直接返回null。
       超时前退出循环后,就从队列移除元素,然后计数器减去一,如果减去1前队列元素大于1则说明当前移除后队列还有元素,那么就发信号激活其他可能阻塞到当前条件信号的线程。
       最后如果减去1前队列元素个数=最大值,那么移除一个后会腾出一个空间来,这时候可以激活可能存在的入队阻塞线程。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值