1.    引言

在并发编程中我们有时候需要使用线程安全的队列。如果我们要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现,本文让我们一起来研究下Doug Lea是如何使用非阻塞的方式来实现线程安全队列ConcurrentLinkedQueue的,相信从大师身上我们能学到不少并发编程的技巧。

2.    ConcurrentLinkedQueue的介绍

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现,该算法在Michael
& Scott算法上进行了一些修改, Michael & Scott算法的详细信息可以参见参考资料一

3.    ConcurrentLinkedQueue的结构

我们通过ConcurrentLinkedQueue的类图来分析一下它的结构。

(图1)

ConcurrentLinkedQueue由head节点和tair节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tair节点等于head节点。

1 private
transient
volatile
Node<e> tail = head;

4.    入队列

入队列就是将入队节点添加到队列的尾部。为了方便理解入队时队列的变化,以及head节点和tair节点的变化,每添加一个节点我就做了一个队列的快照图。

(图二)

  • 第一步添加元素1。队列更新head节点的next节点为元素1节点。又因为tail节点默认情况下等于head节点,所以它们的next节点都指向元素1节点。
  • 第二步添加元素2。队列首先设置元素1节点的next节点为元素2节点,然后更新tail节点指向元素2节点。
  • 第三步添加元素3,设置tail节点的next节点为元素3节点。
  • 第四步添加元素4,设置元素3的next节点为元素4节点,然后将tail节点指向元素4节点。

通过debug入队过程并观察head节点和tail节点的变化,发现入队主要做两件事情,第一是将入队节点设置成当前队列尾节点的下一个节点。第二是更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点,理解这一点对于我们研究源码会非常有帮助。

上面的分析让我们从单线程入队的角度来理解入队过程,但是多个线程同时进行入队情况就变得更加复杂,因为可能会出现其他线程插队的情况。如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾节点。让我们再通过源码来详细分析下它是如何使用CAS算法来入队的。

01 public
boolean
offer(E e) {
02   
03         if
(e == null)
throw new
NullPointerException();
04   
05         //入队前,创建一个入队节点
06   
07         Node</e><e> n =
new Node</e><e>(e);
08   
09         retry:
10   
11         //死循环,入队不成功反复入队。
12   
13         for
(;;) {
14   
15             //创建一个指向tail节点的引用
16   
17             Node</e><e> t = tail;
18   
19             //p用来表示队列的尾节点,默认情况下等于tail节点。
20   
21             Node</e><e> p = t;
22   
23             for
(int
hops =
0; ; hops++) {
24   
25             //获得p节点的下一个节点。
26   
27                 Node</e><e> next = succ(p);
28   
29      //next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点
30   
31                 if
(next != null) {
32   
33                    //循环了两次及其以上,并且当前节点还是不等于尾节点
34   
35                     if
(hops > HOPS && t != tail)
36   
37                         continue
retry;
38   
39                     p = next;
40   
41                 }
42   
43                 //如果p是尾节点,则设置p节点的next节点为入队节点。
44   
45                 else
if (p.casNext(null, n)) {
46   
47                   //如果tail节点有大于等于1个next节点,则将入队节点设置成tair节点,更新失败了也没关系,因为失败了表示有其他线程成功更新了tair节点。
48   
49 if
(hops >= HOPS)
50   
51                         casTail(t, n);
// 更新tail节点,允许失败
52   
53                     return
true;
54   
55                 }
56   
57                // p有next节点,表示p的next节点是尾节点,则重新设置p节点
58   
59                 else
{
60   
61                     p = succ(p);
62   
63                 }
64   
65             }
66   
67         }
68   
69     }

从源代码角度来看整个入队过程主要做二件事情。第一是定位出尾节点,第二是使用CAS算法能将入队节点设置成尾节点的next节点,如不成功则重试。

第一步定位尾节点。tail节点并不总是尾节点,所以每次入队都必须先通过tail节点来找到尾节点,尾节点可能就是tail节点,也可能是tail节点的next节点。代码中循环体中的第一个if就是判断tail是否有next节点,有则表示next节点可能是尾节点。获取tail节点的next节点需要注意的是p节点等于p的next节点的情况,只有一种可能就是p节点和p的next节点都等于空,表示这个队列刚初始化,正准备添加第一次节点,所以需要返回head节点。获取p节点的next节点代码如下

1 final
Node</e><e> succ(Node</e><e> p) {
2   
3          Node</e><e> next = p.getNext();
4   
5          return
(p == next) ? head : next;
6   
7      }

第二步设置入队节点为尾节点。p.casNext(null, n)方法用于将入队节点设置为当前队列尾节点的next节点,p如果是null表示p是当前队列的尾节点,如果不为null表示有其他线程更新了尾节点,则需要重新获取当前队列的尾节点。

hops的设计意图。上面分析过对于先进先出的队列入队所要做的事情就是将入队节点设置成尾节点,doug lea写的代码和逻辑还是稍微有点复杂。那么我用以下方式来实现行不行?

01 public
boolean
offer(E e) {
02   
03        if
(e == null)
04   
05          throw
new NullPointerException();
06   
07       Node</e><e> n =
new Node</e><e>(e);
08   
09       for
(;;) {
10   
11          Node</e><e> t = tail;
12   
13          if
(t.casNext(null, n) && casTail(t, n)) {
14   
15             return
true;
16   
17          }
18   
19       }
20   
21     }

让tail节点永远作为队列的尾节点,这样实现代码量非常少,而且逻辑非常清楚和易懂。但是这么做有个缺点就是每次都需要使用循环CAS更新tail节点。如果能减少CAS更新tail节点的次数,就能提高入队的效率,所以doug
lea使用hops变量来控制并减少tail节点的更新频率,并不是每次节点入队后都将 tail节点更新成尾节点,而是当
tail节点和尾节点的距离大于等于常量HOPS的值(默认等于1)时才更新tail节点,tail和尾节点的距离越长使用CAS更新tail节点的次数就会越少,但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长,因为循环体需要多循环一次来定位出尾节点,但是这样仍然能提高入队的效率,因为从本质上来看它通过增加对volatile变量的读操作来减少了对volatile变量的写操作,而对volatile变量的写操作开销要远远大于读操作,所以入队效率会有所提升。

1 private
static
final
int
HOPS = 1;

还有一点需要注意的是入队方法永远返回true,所以不要通过返回值判断入队是否成功。

5.    出队列

    出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用。让我们通过每个节点出队的快照来观察下head节点的变化。

从上图可知,并不是每次出队时都更新head节点,当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。只有当head节点里没有元素时,出队操作才会更新head节点。这种做法也是通过hops变量来减少使用CAS更新head节点的消耗,从而提高出队效率。让我们再通过源码来深入分析下出队过程。

01 public
E poll() {
02   
03            Node</e><e> h = head;
04   
05        // p表示头节点,需要出队的节点
06   
07            Node</e><e> p = h;
08   
09            for
(int
hops =
0;; hops++) {
10   
11                 // 获取p节点的元素
12   
13                 E item = p.getItem();
14   
15                 // 如果p节点的元素不为空,使用CAS设置p节点引用的元素为null,如果成功则返回p节点的元素。
16   
17                 if
(item != null
&& p.casItem(item,
null)) {
18   
19                      if
(hops >= HOPS) {
20   
21                           //将p节点下一个节点设置成head节点
22   
23                           Node</e><e> q = p.getNext();
24   
25                           updateHead(h, (q !=
null) ? q : p);
26   
27                      }
28   
29                      return
item;
30   
31                 }
32   
33                 // 如果头节点的元素为空或头节点发生了变化,这说明头节点已经被另外一个线程修改了。那么获取p节点的下一个节点
34   
35                 Node</e><e> next = succ(p);
36   
37                 // 如果p的下一个节点也为空,说明这个队列已经空了
38   
39                 if
(next == null) {
40   
41               // 更新头节点。
42   
43                      updateHead(h, p);
44   
45                      break;
46   
47                 }
48   
49                 // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
50   
51                 p = next;
52   
53            }
54   
55            return
null;
56   
57      }

首先获取头节点的元素,然后判断头节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走,如果不为空,则使用CAS的方式将头节点的引用设置成null,如果CAS成功,则直接返回头节点的元素,如果不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取头节点。

6.    参考资料

ConcurrentLinkedQueue的实现原理分析的更多相关文章

  1. 聊聊并发(六)——ConcurrentLinkedQueue的实现原理分析

    1. 引言 在并发编程中我们有时候需要使用线程安全的队列.如果我们要实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,另一种是使用非阻塞算法.使用阻塞算法的队列可以用一个锁(入队和出队用同一把 ...

  2. Java:ConcurrentLinkedQueue的实现原理分析

    本文是作者原创,首发于InfoQ:http://www.infoq.com/cn/articles/ConcurrentLinkedQueue 1.    引言 在并发编程中我们有时候需要使用线程安全 ...

  3. Tomcat源码分析——请求原理分析(中)

    前言 在<TOMCAT源码分析——请求原理分析(上)>一文中已经介绍了关于Tomcat7.0处理请求前作的初始化和准备工作,请读者在阅读本文前确保掌握<TOMCAT源码分析——请求原 ...

  4. java并发包&线程池原理分析&锁的深度化

          java并发包&线程池原理分析&锁的深度化 并发包 同步容器类 Vector与ArrayList区别 1.ArrayList是最常用的List实现类,内部是通过数组实现的, ...

  5. Handler系列之原理分析

    上一节我们讲解了Handler的基本使用方法,也是平时大家用到的最多的使用方式.那么本节让我们来学习一下Handler的工作原理吧!!! 我们知道Android中我们只能在ui线程(主线程)更新ui信 ...

  6. Java NIO使用及原理分析(1-4)(转)

    转载的原文章也找不到!从以下博客中找到http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一 ...

  7. 原子类java.util.concurrent.atomic.*原理分析

    原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ...

  8. Android中Input型输入设备驱动原理分析(一)

    转自:http://blog.csdn.net/eilianlau/article/details/6969361 话说Android中Event输入设备驱动原理分析还不如说Linux输入子系统呢,反 ...

  9. 转载:AbstractQueuedSynchronizer的介绍和原理分析

    简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...

随机推荐

  1. poppler交叉编译

    项目须要生成和打开pdf文件,编译了pdflib库,能够使用.可是上面有水印. ..假设是商业用途去水印的话须要获得许可. 没办法.换呗. 然后就是poppler,这个库的功能非常强大,里面更是增加了 ...

  2. softInputMode- 软件盘的设置

    今天遇到一个问题,就是软件盘弹出来以后,会把之前的布局界面整个的挤到屏幕的外面,而且按下返回建以后,这个软件盘占据的空间会留下一个黑色的背景.在网上查找了很多的方法,刚开始都是说,如下方法 <a ...

  3. qt多线程

    为什么要用多线程? 传统的图形用户界面应用程序都只有一个执行线程,并且一次只执行一个操作.如果用户从用户界面中调用一个比较耗时的操作,当该操作正在执行时,用户界面通常会冻结而不再响应.这个问题可以用事 ...

  4. python绘图问题

    论文绘图整理 # coding: utf-8 #来源:https://blog.csdn.net/A_Z666666/article/details/81165123 import matplotli ...

  5. 【MongoDB】The connection between two tables

    In mongoDB, there are two general way to connect with two tables. Manual Connection and use DBRef 1. ...

  6. optionMenu-普通菜单使用

    首先结合如下的代码来看 package com.android.settings; import android.R.integer; import android.app.Fragment; imp ...

  7. openGLES(三)

    着色器语言 ​ 着色器语言基于c/c++语言,但是还是有区别的,它不是面向对象 数据类型概述 ​ 内建的数据类型:浮点型(float).布尔型(bool).整形(int),矩阵(matrix)以及向量 ...

  8. Docker---(9)Docker中容器无法停止无法删除

    原文:Docker---(9)Docker中容器无法停止无法删除 版权声明:欢迎转载,请标明出处,如有问题,欢迎指正!谢谢!微信:w1186355422 https://blog.csdn.net/w ...

  9. "C:\Program Files\Internet Explorer\iexplore.exe" -extoff 无加载项启动IE 浏览器打开时全屏模式

    "C:\Program Files\Internet Explorer\iexplore.exe" -extoff 无加载项启动IE浏览器打开时全屏模式

  10. Android实践 -- Android蓝牙设置连接

    使用Android Bluetooth APIs将设备通过蓝牙连接并通信,设置蓝牙,查找蓝牙设备,配对蓝牙设备 连接并传输数据,以下是Android系统提供的蓝牙相关的类和接口 BluetoothAd ...