ConcurrentLinkedQueueの「Javaの並行プログラミングの芸術」

キューデータ構造は、すでに非常に精通して導入していない、主に他のいくつかのトリックダグ・リーマスターを理解するためのコードに基づいています。

チーム

図状態のオファー
示されているように、多くの人がチームに、なぜ最初のポストは非常に困惑することができ、TAILは、ノード2を指していませんか?答えは効率のためにあります!Σ(゜Д゜っ;)っこのキューはそれを呼び出すことができ?もちろん、それは先入れ先出し(FIFO)のルールに沿ったもので、まだあります。ただ、TAIL変数ノードは、必ずしもその後、マスターが行う方法を見てみましょう、終わりを指していません。

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) {
            // p是tail结点
            if (p.casNext(null, newNode)) {
                // 判断p结点是否是尾结点
                // 这一步不执行的话,不会对整体流程造成影响,至多是多一次循
                // 环,相比CAS操作,更愿意多一次循环
                if (p != t) 
                    // 交换Tail结点,如果CAS更新失败表示已经有其他线程对其进行更新
                    casTail(t, newNode);
                return true;
            }
            // CAS竞争失败,重新循环,竞争失败后q一般不会为null,除非又发生了出队
        }
        // HEAD和TAIL都指向同一个结点
        // 一个线程执行入队,一个线程执行出队,假设前面都没有更新tail和head
        // 执行出队的线程更新HEAD并设置其为自引用
        // 那么就会发生这个条件想要的现象
        else if (p == q)
            // 如果tail发生了改变,那么就为p设置t,并重新寻找
            // 如果tail未发生改变,head发生了改变,保
            // 险方法就是重新从新head开始遍历
            // 注意: -----只要在读取前完成tail发生更新就行了-----
            p = (t != (t = tail)) ? t : head;
        else
            // p != t 表示p不是尾结点,发生的原因是 入队时没有更新尾结点
            // t != (t = tail) 更新tail,如果tail被其他线程修改,则返回true
            // 如果为true,重新将p设置为尾结点(此时尾结点已经更新了)
            // 如果为false,p = q,继续循环下去
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

チームへの初めての場合:

  1. 最初は同じ項目に頭と尾点がnullのノードであります
  2. 尾なし後続ノード
  3. 次のノードは、CASテール提供されてみてください
  4. p == tは、尾の変数を更新していない、直接trueを返し
    ノートオファーは永遠にtrueを返します

第2チームに:

  1. 最初に同じ項目に対するヘッドとテールポイントはヌルノードであるが、次の点ノード2
  2. 後続ノードと尾
  3. P!= qは、次のif文の中へ
  4. P == tはfalseを返し、全体の三項演算子はfalseを返し、P = T
  5. この場合には全く後継ノードPが存在しません
  6. CASは、pの次の試行を設定されています
  7. P!= tは、アップデートのテール・ノードが直接trueを返します

マルチスレッド・バージョンに第2チーム
のチーム、チーム実行スレッドBに実行スレッドA

図のように、初期状態は次のとおりです。

手順は以下の通り:

オーダー スレッド スレッドB
1
2 node1.item == nullで、もし次の文
3 (Q = node1.next)!= NULL、if文の次の
4 P!= qは、else文後続の実行
5 P = Q
6 node2.item!= nullを
7 P!= H(pはノード2です)
8 (Q = p.next)== NULLが、それは最初のノードpに設定され、ノード2として、すなわちヘッドノード
9 自己参照するノード1
10 自己参照のQのでQ = p.next、すなわちnode1.next、== P リターンアイテム
11 Q!= nullを
12 P == Q
13 t != (t = tail),即此时tail是否发生改变:true -> p =tail;false -> p = head

在步骤13,如果有个线程C已经执行了入队且tail发生改变,那么p就直接紧跟着更新后的tail就行了;如果tail没更新,就要设置p = head,然后重新循环遍历。

出队

从图中可以看出每次出队会有两种可能:将首结点的item置空,不移除;或是将移除首结点。总结一下,就是每次出队更新HEAD结点时,当HEAD结点里有元素时,直接弹出HEAD结点内的元素,而不会直接更新HEAD结点。只有当HEAD结点里没有元素时,出队操作才会更新HEAD结点。这种做法是为了减少CAS更新HEAD结点的消耗,从而提高出队效率。

public E poll() {
    restartFromHead:
    for (;;) {
        // p变量可以理解为游标、当前处理的结点,用于遍历的
        // q变量可以理解为p变量的next
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;
            // ① 先判断当前结点的值不为null 且 CAS更新p变量的item值
            if (item != null && p.casItem(item, null)) {
                // ②更新成功后,判断当前结点是否是头结点
                // 这一步主要是为了节省CAS操作,因为少更新一次HEAD结点没什么影响
                if (p != h) 
                    // ③更新头结点
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            // ④ 获取当前结点的下一个结点,判断是否为null
            // 一般发生于只有一个结点的情况
            else if ((q = p.next) == null) {
                // ⑤ 将当前结点设置为自引用
                updateHead(h, p);
                return null;
            }
            // ⑥ 如果当前结点出现自引用
            // 一般发生在一个线程更新了头结点,让p结点自引用时,p才会等于q
            else if (p == q)
                // 重新获取一个头结点
                continue restartFromHead;
            else
                // p = p.next
                p = q;
        }
    }
}

フローチャートの世論調査

第一次出队:

  1. 初始状态所有item不为null,尝试更新结点的item(一般情况下都是成功的),更新成功后结点item为null
  2. 判断 p == h,不满足条件,直接返回item值

第二次出队:

  1. Node1.item为null,进入下个条件
  2. (q = p.next) != null,进入下个条件
  3. p != q,进入下个条件
  4. p = q,重新循环
  5. Node2.item不为null,尝试更新结点的item,更新成功后结点item为null
  6. 判断 p != h,满足条件
  7. 判断p结点(Node2)是否有后继结点,如果有就将后继结点作为HEAD,否则将P作为HEAD
  8. 返回item值

第二次出队多线程版1
线程A和线程B同时执行poll()方法,假设线程A稍快

线程A 线程B
A1. Node1.item为null B1. Node1.item为null
A2. (q = p.next) != null B2. (q = p.next) != null
A3. p != q B3. p != q
A4. p = q,循环 B4. p = q,循环
A5. Node2.item不为null,尝试更新结点item,更新成功,item值为null B5.
A6. 满足条件 p != h B6. Node2.item为null
A7. 判断p结点(Node2)是否有后继结点 B7. (q = p.next) != null
A8. B8. p != q
A9. B9. p = q 循环
A10. B10. Node3.item不为null,尝试更新结点item,更新成功,item值为null
A11. 将后继结点即Node3设置为HEAD B11.
A12. 返回item B12. 满足条件 p != h
A13. B13. 判断p结点(Node2)是否有后继结点
A14. B14. 由于HEAD已经被修改,所以CAS更新失败
A15. B15. 返回item

这里主要是想讲即便HEAD更新发生冲突,有一次没有更新,也不会影响整体的流程,大不了下次出队的时候多出队一个。

第二次出队多线程版2
线程A和线程B同时执行poll()方法,假设线程A稍快

线程A 线程B
A1. Node1.item为null B1. Node1.item为null
A2. (q = p.next) != null B2. (q = p.next) != null
A3. p != q B3. p != q
A4. p = q,循环 B4. p = q,循环
A5. Node2.item不为null,尝试更新结点item,更新成功,item值为null B5.
A6. 满足条件 p != h B6. Node2.item为null
A7. 判断p结点(Node2)是否有后继结点 B7.
A8. 将后继结点即Node3设置为HEAD B8.
A9. 返回item B9. (q = p.next) != null
A10. B10. p == q
A11. B11. 重新获取HEAD并执行遍历

这个例子主要表达了当A线程先修改了首结点,并将原来的首结点设置为自引用时,B线程在循环过程中会执行到一条语句(q = p.next),然后在下一个条件语句中进入continue restartFromHead,重新获取HEAD变量并遍历

总结

ConcurrentLinkedQueue主要内容都已经学习过了,其中分析的过程花费了一个早上,吃完饭回来坐下才有了一些思路。学习的难点主要还是在它不同于普通的队列,它的tail和head变量不会时刻指向头结点和尾结点,这也造就了代码的复杂性,否则如下所示即可:

public boolean offer(E item){
    checkNotNull(item);
    for(;;){
        Node<T> node = new Node(item);
        if(tail.casNext(null, node) && casTail(tail, node)){
            return true;
        }
    }
}

但是这样和上面的例子比起来,就有性能的差距,差距主要体现在CAS写竞争方面:

最悲观的角度,ConcurrentLinkedQueue的offer方法需要执行两次 CAS (假设不发生竞争,其实我觉得不会有CAS竞争发生),上面的通用代码方法也需要执行两次,这里持平。
最乐观的角度,ConcurrentLinkedQueue只需要执行一次CAS,上面的通用方法仍需要两次。
原本是参考《Java并发编程的艺术》,但是里面的实现和现在不同了,所以根据现在的实际情况写了一份。当然,里面的主线思路仍然没有发生改变——尽量减少CAS操作。书上的代码是通过hops变量来控制多久需要更新一次值,大致思路如下所示:

遍历前,hops = 0
HEAD---
      |
    Node1 -> Node2 -> Node3 -> Node4
      |
TAIL---

假设现在要插入Node5,就要从TAIL变量位置(Node1位置)开始往后遍历,总共要循环三次才能找到最后一个尾结点,此时计数器hops就等于3,当Node5插入成功后,判断hops的值是否达到阈值,如果达到了,就更新tail变量;反之则不更新,直接返回。

遍历完后,hops = 3,达到阈值(假设达到了),将tail变量更新给Node5
HEAD---
      |
    Node1 -> Node2 -> Node3 -> Node4 -> Node5
                                          |
                                    TAIL---

ConcurrentLinkedQueue初看以为很简单,其实逻辑还是挺复杂的,拓展了对队列的看法。今天在写这篇博客时,感觉一头雾水,因为CAS操作不像锁那样简单,代码块锁住就能放心执行,CAS只对单个操作保证可见性和原子性,很担心后面的线程会对其进行什么修改,今天过后总结了一下写并发容器的思路:

  1. 在了解某个方法的实现时,需要分清局部变量和共享变量,在理清了局部变量的含义后,将重点放在共享变量上
  2. 文の方法は理解していなかった場合は(損失を入力し、突然、このような文が来た)、マルチスレッド方向に考えてください。
  3. テーブルmdの例で書かれたマルチスレッド、マルチメソッド分析(アイデアを明確にすることができます)、それはあなたが感じるMD読みにくい形成している場合、あなたはこのサイトで見ることができ、明らかであるMDフォームビルダ

おすすめ

転載: www.cnblogs.com/codeleven/p/10963032.html