参考及引用声明:
Java多线程进阶(二九)—— J.U.C之collections框架:ConcurrentLinkedQueue
(推荐)Java并发编程之ConcurrentLinkedQueue详解
一,我们要知道ConcurrentLinkedQueue是个啥?
在并发编程中,一般需要用到安全的队列,如果要自己实现安全队列,可以使用2种方式:
- 方式1:加锁,这种实现方式就是我们常说的阻塞队列。(如 BlockingLinkedQueue)
- 方式2:使用循环CAS算法实现,这种方式实现队列称之为非阻塞队列。(如 ConcurrentLinkedQueue)
ConcurrentLinkedQueue是JDK1.5时随着J.U.C一起引入的一个支持并发环境的基于链表实现的队列。(基于链接节点的无界线程安全的队列,按照先进先出原则对元素进行排序。新元素从队列尾部插入,而获取队列元素,则需要从队列头部获取。)
二,CLQ是如何实现“安全”的?
没有利用锁或底层同步原语(无锁算法),而是完全基于自旋+CAS的方式实现了该队列。
扩展:CAS和自旋
来源:
https://blog.csdn.net/weixin_44594056/article/details/90552443
https://blog.csdn.net/qq_36071795/article/details/83818320
CAS:CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:它包含 3 个参数 CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。
CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。(ABA问题--通过版本号解决)
自旋:就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。
(自适应自旋锁:线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数)
好,我们继续回来扒这个ConcurrentLinkedQueue。
由于是完全基于无锁算法实现的,所以当出现多个线程同时进行修改队列的操作(比如同时入队),很可能出现CAS修改失败的情况,那么失败的线程会进入下一次自旋,再尝试入队操作,直到成功。所以,在并发量适中的情况下,ConcurrentLinkedQueue一般具有较好的性能。
ConcurrentLinkedQueue内部就是一个简单的单链表结构,每入队一个元素就是插入一个Node类型的结点。字段head
指向队列头,tail
指向队列尾,通过Unsafe
来CAS操作字段值以及Node对象的字段值。
由于队列本身是一种链表结构,所以虽然算法看起来很简单,但其实需要考虑各种并发的情况,实现复杂度较高,并且ConcurrentLinkedQueue不具备实时的数据一致性,实际运用中,队列一般在生产者-消费者的场景下使用得较多,所以ConcurrentLinkedQueue的使用场景并不如阻塞队列那么多。
扩展:Unsafe类
来源:https://www.jianshu.com/p/2e5b92d0962e
- Java 不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe 类提供了硬件级别的原子操作。
- Unsafe无法实例化(构造方法私有)&#x