Java 并发包中并发队列原理剖析(一)ConcurrentLinkedQueue、LinkedBlockingQueue

    JDK 提供了一系列场景的并发安全队列。总的来说,按照实现方式的不同 可分为 阻塞队列 和 非阻塞队列,前者使用锁🔒实现,后者使用 CAS 非阻塞算法实现。

一、ConcurrentLinkedQueue

     ConcurrentLinkedQueue 是线程安全的无界非阻塞队列,其底层数据结构使用单向链表实现,对于入队 和 出队,使用 CAS 实现线程安全。

1、类图

在这里插入图片描述
     ConcurrentLinkedQueue 内部的队列使用单向链表的方式实现,其中有两个 volatile 类型的 Node 节点 分别用来存放队列的首 head、尾 tail 节点:

private transient volatile Node<E> head;
private transient volatile Node<E> tail;

ConcurrentLinkedQueue无参构造源码:

 public ConcurrentLinkedQueue() {
 		                  // (1)
        head = tail = new Node<E>(null);
    }

(1):

Node(E item) {
            UNSAFE.putObject(this, itemOffset, item);
        }

    可以看到,默认头、尾节点都是指向 item 为 null值 的哨兵节点

Node 是 ConcurrentLinkedQueue 的 静态内部类,源码:

private static class Node<E> {
        volatile E item;
        volatile Node<E> next;

    可以看到,在 Node 节点内部维护一个使用 volatile 修饰的变量 item,用来存放节点的值,next 用来存放链表的下一个节点。新元素会被插入到队列末尾,出队时从队列头部获取一个元素,内部使用 UNSAFE 工具类提供的 CAS 算法保证出入队时操作链表的原子性。
    

2、ConcurrentLinkedQueue 原理介绍
(1) offer 操作

源码:

  public boolean offer(E e) {

		// (一)e 为 null 则抛出空指针异常
        checkNotNull(e);
        
        // (二)
        final Node<E> newNode = new Node<E>(e);

		// (三)
        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;

			// (四)如果 q == null,说明 p 是尾节点
            if (q == null) {
            
            	// (五)使用 CAS 设置 p 节点的 next 节点
                if (p.casNext(null, newNode)) {
              
              		// (六)
                    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)
                // (七)多线程操作时,由于 poll 操作移除元素后 可能会把 head 变成自引用,也就是 head 的 next 变成了 head,所以这里需要重新找新的 head
                p = (t != (t = tail)) ? t : head;
                
            else
               // (八)寻找尾节点
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

    对于线程第一次添加元素,分析如下:(一),对传入的参数进行空检查,如果为 null,则抛出 NPE 异常。否则执行(二),并使用 e 作为构造方法的参数,创建一个新的节点,然后 (三)从尾部开始循环,打算从尾部添加元素,执行到 (四)时,如图所示:
在这里插入图片描述

    这时节点 p、t、head、tail 同时指向了 item 为 null 的哨兵节点,由于哨兵节点的 next 节点为 null,所以 q 也指向 null。(四) 成立,接下来执行 (五),提供 CAS 操作 判断 p 节点的 next 节点是否为 null,如果为 null ,则使用节点 newNode 替换 p 的 next 节点,然后执行(六)由于 p 是 == t 的,所以不会设置尾节点 tail,接下来就返回 true 退出 offer 方法。

在这里插入图片描述
    这是一个线程调用 offer 方法的情况,如果多个线程同时调用,就会存在多个线程同时执行到(五)的情况。假设线程 A 调用 offer(e1),线程 B 调用 offer(e2),同时执行到 (五) if (p.casNext(null, newNode)) 。由于 CAS 的比较设置操作是原子性的,假设线程 A 先执行了比较设置操作,发现当前 p 的 next 节点确认是 null,则会原子性更新 next 节点为 e1 ,这时线程 B 也会判断 p 的 next 节点是否为 null,结果发现不是,就会跳到 (三),然后执行代码 (四):
在这里插入图片描述

    再下来,线程 B 的代码应该执行(八),把 q 赋给 p:
在这里插入图片描述

    然后线程 B 再次跳到(三)执行 Node<E> q = p.next;
在这里插入图片描述
    这时 q == null 成立,所以线程 B 会执行代码 (五),通过 CAS 操作,判断当前 p 的 next 节点是否为 null,不是则再次循环尝试,是的话则使用 e2 替换。假设 CAS 成功了,(也就是说 CPU 没给线程 A,就是这种情况,是可以 CAS 成功的。) 那么执行 (六),由于 p != t ,所以设置 tail 节点为 e2,返回退出 offer 方法:
在这里插入图片描述
    
    分析至此,(七)没有走过,其实这一步需要在执行 poll 操作后才会执行。这里来看一下执行 poll 操作后可能会存在的一种情况:(等会儿讲 poll 方法时就会见到)
在这里插入图片描述
    这时执行 offer 方法,执行到(三)时:
在这里插入图片描述

    接下来,q == null 不成立,而且 p == q ,所以执行(七),t = tail ,所以会把 head 赋给 p,然后继续循环,到了 (三),会把 p.next 也就是 NULL 赋给 q:
在这里插入图片描述
    接下来到了(四),由于 q == null 成立,所以会执行 (五),进行 CAS 操作,如果当前没有其他线程执行 offer 操作,则 CAS 就会成功,p 的 next 节点被设置为新增节点,然后执行代码(六),由于 p != t ,所以设置新节点为队列的尾部:
在这里插入图片描述
    然后哪个自引用的节点就会被垃圾回收掉。
    可见,offer 操作中的关键步骤是 (五),通过原子 CAS 操作来控制某时只有一个线程可以追加元素到末尾,直到 CAS 成功了才会返回,也就是通过无限循环不断进行 CAS 尝试方式来替代阻塞算法挂起调用线程。相比阻塞算法,这是使用 CPU 资源换取阻塞所带来的开销。
    

(2) add 操作

    add 操作是在链表末尾添加一个元素,其实在内部调用的还是 offer 操作。

public boolean add(E e) {
        return offer(e);
    }

(既生 offer 何生 add ❔❔❔)
    

(3) poll 操作

    在队列头部获取并移除一个元素,如果队列为空 则返回 null。

源码:

   public E poll() {
   		// (一)goto 标记
        restartFromHead:
        
        // (二)
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
            	// (三)保存当前节点值
                E item = p.item;

				// (四)当前节点有值 则通过 CAS 变为 null
                if (item != null && p.casItem(item, null)) {
                    
                    // (五)CAS 成功则标记当前节点 并从链表中移除
                    if (p != h) // hop two nodes at a time
                    	
                    	//(六)
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }

				// (七)当前队列为空 则返回 null
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }

				// (八)如果当前节点被自引用了,则重新寻找新的队列头节点
                else if (p == q)
                    continue restartFromHead;
                else 
                	// (九)
                    p = q;
            }
        }
    }

(六)updateHead:

final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p))
            h.lazySetNext(h);
    }

    队列一开始为空时:
在这里插入图片描述
    进入循环。 因为 item == null ,所以代码走到 (七),假设这个过程中没有线程调用 offer 方法,则此时 q 等于 null :
在这里插入图片描述
     下来该执行 updateHead(h, p) 方法,由于 h == p ,所以没有设置头节点,poll 方法直接返回 null 。
     假设执行到 (七)时,已经有其他线程调用了 offer 方法 并成功添加一个元素到队列,这时候 q 指向的是新增元素的节点:
在这里插入图片描述
    这时 ,(七)判断结果为 false,继续执行代码 (八),此时 p 不等于 q, 执行代码 (九),p 指向 q:
在这里插入图片描述

    然后继续循环,到(四),通过 CAS 操作尝试设置 p 的 item 值为 null,如果此时没有其他线程进行 poll 操作,则 CAS 成功,执行 (五),由于 p 不等于 h,所以设置头节点为 p,并设置 h 的next 节点为它自己,然后返回从队列中移除的节点值 item。
在这里插入图片描述
    现在代码中的分支 (八)还没有走过,什么时候会执行呢?假设线程 A 执行 poll 操作时,当前队列状态是:
在这里插入图片描述
    那么执行 到(四)p.casItem(item, null) 通过 CAS 操作 尝试设置 p 的 item 值为 null:
在这里插入图片描述

    假设 CAS 设置成功,这时 p != h ,所以会执行 (六)updateHead 方法,假设线程 A 执行 updateHead 前,另一个线程 B 开始 poll 操作,这时 线程 B 的 p 指向 head 节点,但是还没有执行到(七):
在这里插入图片描述
    然后线程 A 执行 updateHead ,执行完毕后线程 A 退出:
在这里插入图片描述

    然后线程 B 继续执行代码(七),q = p.next ,由于该节点是自引用节点,所以 p==q ,就会执行 (八),跳到外层循环,然后获取当前队列头节点 head :

在这里插入图片描述
🎭总结:
    poll 是简单地使用 CAS 操作把当前节点的 item 值设置为 null,然后通过重新设置头节点将该元素从队列里移除,被移除的节点就成了孤立节点,这个节点会在垃圾回收时被回收掉。另外,如果在执行分支中发现头节点被修改了,要跳到外层循环重新获取新的头节点。
    

(4) peek 操作

    获取队列头部一个元素(只获取 不移除),如果队列为空 则返回 null。

源码:

 public E) {
 		// (一)
        restartFromHead:
        
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
            	// (二)
                E item = p.item;

				// (三)
                if (item != null || (q = p.next) == null) {
                    updateHead(h, p);
                    return item;
                }
                // (四)
                else if (p == q)
                    continue restartFromHead;
                else
                	// (五)
                    p = q;
            }
        }
    }

     peek 操作与 poll 操作类似,不同之处在于 (三) 处少了 casItem(item, null) ,因为 peek 只是获取队头元素值,并不清空值。
    执行到 (三):
在这里插入图片描述

     接下来执行 updateHead(h, p); ,由于 h 节点等于 p 节点,所以不进行任何的操作,然后 peek 操作会返回 null。
    当队列中至少有一个元素时,假设只有一个;
在这里插入图片描述
    这时会执行到 (四),但是 p != q,所以会执行(五),p 指向 q:
在这里插入图片描述
    继续循环,到 (三),item != null,所以执行 updateHead(h, p); ,设置 头节点:
在这里插入图片描述
    也就是剔除了哨兵节点。

🎭总结
    peek 操作的代码与 poll 相似,只是前者只获取队列头元素 但是并不从队列里将它剔除,而后者 获取后需要从队列中将它剔除。另外,第一次调用 peek 方法时,会删除 哨兵节点 ,并让队列的 head 节点指向队列里第一个元素 或者 null 。
    

(5)size 操作

    计算当前队列元素个数,在并发环境下不是很有用,因为 CAS 没有加锁,所以从调用 size 方法到返回结果期间有可能增删元素,导致统计的个数不精确。

源码:

 public int size() {
        int count = 0;

		//               (一)                   (二) 
        for (Node<E> p = first(); p != null; p = succ(p))
        
            if (p.item != null)
                // Collection.size() spec says to max out
                if (++count == Integer.MAX_VALUE)
                    break;
        return count;
    }

(一):获取第一个元素(哨兵元素不算),没有则为 null

 Node<E> first() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                boolean hasItem = (p.item != null);
                if (hasItem || (q = p.next) == null) {
                    updateHead(h, p);
                    return hasItem ? p : null;
                }
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }

(二):获取当前节点的 next 元素,如果是自引入节点 则 返回真正的头节点

final Node<E> succ(Node<E> p) {
        Node<E> next = p.next;
        return (p == next) ? head : next;
    }

 

(6)remove 操作

     如果队列里存在该元素,则删除该元素,如果存在多个则删除第一个,并返回 true,否则返回 false。

  public boolean remove(Object o) {
        if (o != null) {
            Node<E> next, pred = null;
            for (Node<E> p = first(); p != null; pred = p, p = next) {
                boolean removed = false;
                E item = p.item;
                if (item != null) {
                    if (!o.equals(item)) {
                    	// 获取 next 元素
                        next = succ(p);
                        continue;
                    }
                    removed = p.casItem(item, null);
                }

                next = succ(p);
                // 如果有前驱节点,并且 next 节点不为空 则链接前驱节点到 next 节点
                if (pred != null && next != null) // unlink
                    pred.casNext(p, next);
                if (removed)
                    return true;
            }
        }
        return false;
    }

    

(7) contains 操作

     判断队列中是否含有指定对象,由于是遍历整个队列,所以像 size 操作一样,结果也不精确,有可能调用该方法的时候 元素还在队列里面,但是遍历过程中 其他线程把元素删除了,那就会返回 false 了。

源码:

public boolean contains(Object o) {
        if (o == null) return false;
        for (Node<E> p = first(); p != null; p = succ(p)) {
            E item = p.item;
            if (item != null && o.equals(item))
                return true;
        }
        return false;
    }

    
🎭总结 :

     ConcurrentLinkedQueue 的底层使用单向链表数据结构,每个元素被包装成一个 Node 节点。队列靠头、尾节点维护,创建队列时 头、尾系欸但指向一个 item 为 null 的哨兵节点。第一次执行 peek 或者 first 操作时,会把 head 指向第一个真正的队列元素。由于使用非阻塞 CAS 算法,没有加锁,所以在计算 size 时有可能进行了 offer、poll 或者 remove 操作,导致计算的个数不准确,所以在并发情况下, size 方法不是很有用。
     入队、出队都是使用 volatile 修饰的 tail、head 节点,要保证在多线程下出入队线程安全,只需要保证这两个 Node 操作的可见性 和 原子性即可。由于 volatile 保证可见性,所以只需要保证对两个变量操作的原子性即可。
     offer 操作是在 tail 后面添加元素,也就是调用 tail.casNext 方法,而这个方法使用的是 CAS 操作,只有一个线程会成功,然后失败的线程会循环,重新获取 tail,再只需 casNext,poll 也是通过类似的 CAS 算法保证出队时 移除节点操作的原子性。

&#160;&#160;&#160;&#160;

二、LinkedBlockingQueue

1、类图

在这里插入图片描述
    可以看到,LinkedBlockingQueue 也是使用单向链表实现的,也有两个 Node,分别用来存头、尾节点,还有个初始值为 0 的原子变量 count,用来记录队列元素个数。另外还有两个 ReentrantLock 的实例,分别用来控制元素入队和出队的原子性,其中 takeLock 用来控制同时只有一个线程可以从队列头获取元素,其他线程必须等待,putLock 控制同时只能有一个线程可以获取锁,在队列尾部添加元素,其他线程必须等待。还有 notFull 和 notEmpty 是条件变量,内部都有一个条件队列用来存放出队和入队被阻塞的线程,其实这就是 生产者-消费者模型。
    
无参构造源码:

 public LinkedBlockingQueue() {
  		// (一) 队列默认容量是 0x7fffffff
        this(Integer.MAX_VALUE);
    }

    队列的容量用户也可以自己指定的,从一定程度上说,LinkedBlockingQueue 是有界阻塞队列。
(一):

public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;

		// 初始化头、尾节点,让它们指向哨兵节点
        last = head = new Node<E>(null);
    }

    

2、LinkedBlockingQueue 原理介绍
(1) offer 操作

    向队列尾部插入一个元素,如果队列中有空闲则插入成功后返回 true,如果队列已满,则丢弃当前元素,然后返回 false,如果 e 元素为 null 则抛出 NullPointerException 异常。该方法是非阻塞的。

源码:

     public boolean offer(E e) {

		// 为空则抛出空指针异常
        if (e == null) throw new NullPointerException();
        
        final AtomicInteger count = this.count;

		// 如果当前队列满则丢弃将要放入的元素,然后返回 false
        if (count.get() == capacity)
            return false;
            
        int c = -1;

		// 构建新节点
        Node<E> node = new Node<E>(e);

		// 获取 putLock 独占锁
        final ReentrantLock putLock = this.putLock;
        
        putLock.lock();
        try {
        
        	// 重新判断当前队列是否满,如果队列不满则进队
            if (count.get() < capacity) {
                enqueue(node);

				// 递增元素计数
                c = count.getAndIncrement();

				// 如果新元素入队后队列还有空闲空间,则唤醒 notFull 的条件队列里因为调用了 notFull 的 await 操作而被阻塞的一个线程
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
        	// 释放锁
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }
(2) put 操作

     向队列尾部插入一个元素,如果队列中有空闲则插入后直接返回,如果队列已满,阻塞当前线程,直到队列有空闲,插入成功后返回。如果在阻塞时 被其他线程设置了中断标志,则被阻塞线程会抛出 InteruptedException 异常并返回。另外,如果插入的元素为 null 则抛出 NullPointerException 异常。
源码:

     public void put(E e) throws InterruptedException {

        if (e == null) throw new NullPointerException();
        
        int c = -1;
		// 创建新节点,并获取独占锁 putLock
		// private final ReentrantLock putLock = new ReentrantLock();
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        
        final AtomicInteger count = this.count;

		// 获取独占锁,可以被中断
        putLock.lockInterruptibly();
        try {
         
         	// 如果队列满,则把当前线程放入 notFull 的条件队列,线程被阻塞挂起
         	// while 循环,避免虚假唤醒
            while (count.get() == capacity) {
                notFull.await();
            }

			// 入队并递增计数
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

    

(3)poll 操作

    从队列头部获取并移除一个元素,如果队列为空,则返回 null,该方法是不阻塞的。poll 逻辑毕竟简单,值得注意的是,获取元素时 只操作了队列的头节点。
源码:

  public E poll() {
        final AtomicInteger count = this.count;

		// 队列为空则返回 null
        if (count.get() == 0)
            return null;
            
        E x = null;
        int c = -1;

		// 获取独占锁 takeLock 
		// private final ReentrantLock takeLock = new ReentrantLock();
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {

			// (一)队列不为空则出队 并递减计数
            if (count.get() > 0) {
            
            	// (二)
                x = dequeue();
                
                // (三)
                c = count.getAndDecrement();

				// 如果计数值大于 1 ,说明当前线程移除队列里的一个元素后 队列不为空
				// 这时就可以激活 调用了 take 方法 但是因为队列满 而被阻塞到 notEmpty 的条件队列里的一个线程
                if (c > 1)
                    notEmpty.signal();
                    
            }
        } finally {
            takeLock.unlock();
        }
		// 如果当前线程移除队头元素前当前队列是满的,那么移除队头元素后 当前队列至少有一个空闲位置
        if (c == capacity)
        	// 那么就可以调用 signalNotFull 激活因为调用 put 方法但是队满 而 阻塞到 notFull 的条件队列里的一个线程
            signalNotFull();
        return x;
    }

(三):

/**
     * Atomically decrements by one the current value.
     *
     * @return the previous value
     */
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }

     注释里说, getAndDecrement() 方法原子性地把当前值减 1,返回原先的值。 注意,c 存放的是 count 递减前的值,也就是 当前线程移除元素前 队列的元素个数。

     注意到,(一)判断当前队列不为空,则进行出队操作,然后(三)递减计数器,❓ 如何保证 (一)时队列不为空,而执行到 (二)时 队列也一定不为空呢,会不会出现 (一)判断队列不为空了,但是执行到(二)时,其实队列已经空了呢 ❓ 毕竟这不是原子性操作。主要看在 (二)之前 ,哪些地方会修改 count 的计数。由于当前线程已经拿到了 takeLock 锁,所以其他调用 poll 或者 take 方法的线程 不可能去修改 count 了,要是有线程在修改 count,那应该是调用了 put 和 offer 方法,因为 put 和 poll 获取的是 putLock 锁而不是 takeLock 锁,但是 put 和 offer 操作内部是增加 count 计数值的,所以肯定不会出现 (一)判断队列为空,而到了(二)时队列空了的情况。其实只需要看在哪些地方,递减了 count 值即可,只有递减了 count 才会出现上面的情况。只有 poll、take 或者 remove 方法中会递减 count 值,但是这三个方法都需要获取到 takeLock 才能进行操作,而当前线程已经获取了 takeLock 锁,所以其他线程没有机会在当前情况下递减 count 计数值,所以即使 (一)、(二)不是原子性的,但是它们是线程安全的。
    

(4)peek 操作

     获取队列头部元素,但是不从队列里移除它,如果队列为空 则返回 null,该方法是不阻塞的。

源码:

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

     (一) 和 (二)又不是原子性操作,也就是说,在执行 (一) ,判断队列不为空后,(二)获取锁之前,可能有其他线程执行了 poll 或者 take 操作 导致队列变为 空,然后当前线程获取锁后,继续往下执行,执行到 (四),就会抛出空指针异常。 这就是 poll 和 peek 方法的不同之处
    

(5)take 操作

     获取当前队列头部元素 并从队列里移除它,如果队列为空,则阻塞当前线程,直到队列不为空,然后返回元素,如果在阻塞时被其他线程设置了中断标志,则被阻塞线程会抛出 InterruptedException 异常而返回。

源码:

  public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;

		// 获取锁
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
        	// 如果当前队列为空 则阻塞挂起,并把当前线程放入 notEmpty 的条件队列
            while (count.get() == 0) {
                notEmpty.await();
            }

			// 出队并递减计数
            x = dequeue();
            c = count.getAndDecrement();
            
            // 如果当前队列不为空,就唤醒因为调用 take 但队空而被阻塞到 notEmpty 的条件队列里的一个线程
            if (c > 1)
                notEmpty.signal();
                
        } finally {
            takeLock.unlock();
        }

		// 如果当前线程移除队头元素前当前队列是满的,那么移除队头元素后 当前队列至少有一个空闲位置
        if (c == capacity)
        	// 那么就可以调用 signalNotFull 激活因为调用 put 方法但是队满 而 阻塞到 notFull 的条件队列里的一个线程
            signalNotFull();
        return x;
    }

    

(6)remove 操作

     删除队列里面指定的元素,有 则删除 并返回 true,没有则返回 false。在删除指定元素前加了两把锁,所以在遍历队列查找指定元素的过程中,是线程安全的,并且此时其他调用入队、出队操作的线程全部会被阻塞。

源码:

  public boolean remove(Object o) {
        if (o == null) return false;

		// (一)双重加锁
        fullyLock();
        
        try {

			// 遍历队列 找到则删除 并返回 true
            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;
                }
            }
            // 找不到则返回 false
            return false;
            
        } finally {
        	// (三)
            fullyUnlock();
        }
    }

(一) 获取双重锁,获取后,其他线程进行入队 或者 出队 操作时 就会被阻塞挂起:

void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }

(二):

void unlink(Node<E> p, Node<E> trail) {
    // assert isFullyLocked();
    // p.next is not changed, to allow iterators that are
    // traversing p to maintain their weak-consistency guarantee.
    p.item = null;
    trail.next = p.next;
    if (last == p)
        last = trail;
    if (count.getAndDecrement() == capacity)
        notFull.signal();
}

(三):与加锁顺序相反 的顺序释放双重锁

void fullyUnlock() {
        takeLock.unlock();
        putLock.unlock();
    }
(7)size 操作

源码:

  public int size() {
        return count.get();
    }

     获取当前队列元素个数。 由于入队、出队 操作的 count 都是加了锁的,所以结果相比 ConcurrentLinkedQueue 的 size 方法比较准确,
    

🎭总结 :
    LinkedBlockingQueue 的内部是通过 单向链表实现的,使用头、尾 节点来进行入队和出队操作。对头、尾的操作分别使用了单独的独占锁 从而保证了原子性,所以入队和出队是可以同时进行的。另外,对头、尾节点 的独占锁都配备了一个条件队列,用来存放被阻塞的线程,并结合入队、出队实现了一个生产消费模型。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值