Deadly Javaコンカレントプログラミング(9):無制限のスレッドセーフキューConcurrentLinkedQueueソースコード分析

この記事は理解するのは難しくありませんが、ConcurrentHashMapと比較すると、拡張やデータ移行などの操作が含まれていないため、よりシンプルであり、読んだ後に確実に得られると思います。

この記事は9つのSike Java Concurrencyシリーズであり、主役はJava ConcurrentLinkedQueueであり、スレッドセーフで無制限のキュー(達成するリンクリストに基づいているため、無制限であるため)であり、並行プログラミングで必要になることが多いスレッドセーフキューの場合、スレッドプールをインタビューするときに、キュー内のキューもこのキューを使用して実装できます。これはスレッドセーフであり、先入れ先出しの順序を使用します。

一連の並行性に関する記事全体を検討することで、スレッドセーフキューを実装する2つの方法を考えることができます。1つは、ブロッキングメソッドを使用すること、つまり、ロックの形式を使用すること、デキューメソッドとエンキューメソッドで同期を追加するか、ロックを取得することです。ロックなどで実現 もう1つはノンブロッキング方式で、スピンCASを使用して実現されます。Java並行処理パッケージでは、並行処理の最初のクラスが並行処理をサポートしていることがわかります。これは非ブロッキングです。

この記事では、並行処理マスターのDoug Leaが非ブロッキングメソッドを使用して、ソースコード分析からスレッドセーフキューConcurrentLinkedQueueを実装する方法を分析しましょう。マスターから多くの並行処理スキルを学ぶことができると思います。

ConcurrentLinkedQueue構造

ConcurrentLinkedQueueのクラス図を通じてその構造を分析します。

ここに画像の説明を挿入

ConcurrentLinkedQueueはヘッドノードとテールノードで構成されており、ノードのサブクラスである各ノードはノード属性項目と次(次のノードノードへの参照)で構成されていることがわかります。つまり、リンクされたリストを形成するために、ノードは次によって関連付けられます。

エンキューとは、要素をノードにパックし、リンクされたリストの最後にその都度要素を配置することを意味します。デキューすると、リンクされたリストの先頭から要素が削除されて戻ります。

全体的な構造を理解した後、ConcurrentLinkedQueueの最も重要なことは、エンキューとデキューの2つの操作であることがわかります次に、ソースコードレベルから直接学習します。

エンキュー操作

チームに参加するプロセスでは、実際には参加ノードをキューの最後に追加します。実際、マスターDoug Leaは、この参加操作のためにいくつかの最適化を行っています。ソースコードをより明確に示すために、チームに参加する一連のプロセス図を以下に示します。いくつかの最適化ポイント。ここで4つのノードを挿入するとします。

ここに画像の説明を挿入

上記のチームに入る操作を通じて、ヘッドノードとテールノードの変化を観察しました。結論は、実際には2つあります。1つ目は、エンキューノードを現在のテールノードの次のノードに設定すること、2つ目は、テールノードを更新することです。テールノードの次のノードが空でない場合は、エンキューノードをテールノードとして設定し、テールノードの次のノードが空の場合は、エンキューノードをテールノードの次のノードとして設定しますつまり、テールノードは必ずしもエンドノードである必要はなく、明確に覚えておく必要があり、以下のキューのソースコードを理解するのに非常に役立ちます。

以下ではあまり言いませんが、コードを直接見て、上記の説明を理解し、コードのコメントを組み合わせるだけで、理解できると思います。


// 将指定的元素插入到此队列的末尾,因为队列是无界的,所以这个方法永远不会返回 false 。
public boolean offer(E e) {
    
    
    checkNotNull(e);
    // 入队前,创建一个入队节点
    final Node<E> newNode = new Node<E>(e);
    // 死循环,入队不成功反复入队
    // 创建一个指向tail节点的引用,p用来表示队列的尾节点,默认情况下等于tail节点
    for (Node<E> t = tail, p = t;;) {
    
    
        // 获得p节点的下一个节点
        Node<E> q = p.next;
        // next节点为空,说明p是尾节点
        if (q == null) {
    
    
            // p is last node
            // p是尾结点,则设置p节点的next节点为入队节点
            if (p.casNext(null, newNode)) {
    
    
                // 首先要知道入队操作不是每次都设置tail节点为尾结点,为了减少CAS操作提高性能,也就是说tail节点不总是尾节点
                // 如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点,
                // p != t 这个条件者结合下面 else 分支看,下面在冲突的时候会修改 p 指向 p.next,所以导致p不等于 tail,
                // 即tail节点有大于等于1个的next节点
                if (p != t) // hop two nodes at a time
                    // 如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点,
                    // 这里允许失败,更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        // 这个分支要想进去,即 p 节点等于 p.next 节点,只有一种可能,就是 p 节点和 p.next 节点都为空
        // 表示这队列刚初始化,正准备添加节点,所以需要返回head
        else if (p == q)
            // 如果tail变了,说明被其他线程添加成功了,则 p 取新的 tail,否则 p 从 head 开始
            p = (t != (t = tail)) ? t : head;
        else
            // Check for tail updates after two hops.
            // 进行这个分支说明next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点
            // 执行 p != t 说明尾结点不等于tail,t != (t = tail)) 说明tail做了变动,
            // 同时满足说明tail已经重新设置了,即结尾就是tail,否则尾结点取tail.next
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

ソースコードの観点から見ると、エンキュープロセス全体では主に2つの処理が行われます。1つ目はテールノードを見つけること、2つ目はCASアルゴリズムを使用してエンキューノードをテールノードの次のノードとして設定し、失敗した場合は再試行することです。

ここで考えてみましょう。上記の入口と出口の操作の分析は、先入れ先出しキューのエンドノードとして設定することです。マスターダグリーのコードは少し複雑です。次のコードに置き換えることはできますか?

public boolean offer(E e) {
    
    
    if (e == null) 
        throw new NullPointerException();
    Node<E> n = new Node<E>(e); 
    for (;;) {
    
    
        Node<E> t = tail;
        if (t.casNext(null, n) && casTail(t, n)) {
    
     
            return true; 
        } 
    } 
}

上記のコードは、チームに入るたびにテールをエンドノードとして設定します。これにより、コードが大幅に節約され、理解しやすくなります。
ただし、これの欠点は、ループCASがテールノードの設定に毎回使用されることです。CASを削減してテールノードを更新できる場合、エンキューの効率を向上できます。ただし、テールは必ずしもテールノードに等しいとは限らないため、エンドノードを見つけるためにキューに入るときに、もう1つのループ操作が必要であることも考慮する必要があります。

ただし、揮発性変数のノードテールであるため、この効率は依然として高く、読み書きの揮発性変数の揮発性変数を減らすために増加を通過することが自然の観点であり、オーバーヘッドの場合、揮発性書き込みは読み取り操作よりもはるかに大きいため、チームは効率が向上します。偉大なる神のパフォーマンスの追求は極めて現実的なものであり、ソースコードは依然として読むのに役立ちます!

デキュー操作

キューからの取り出し操作について言えば、キュ​​ーから取り出して更新ヘッドノードを減らし、キューのヘッドを直接ポップアップしてCAS更新操作を減らしてパフォーマンスを向上させる必要があると間違いなく思いますか?

読者の理解を容易にするために、デキュー操作のスナップショットのセットを次に示します。

ここに画像の説明を挿入

図を見ると、チームを去るたびにヘッドノードが更新されないことがわかります。ヘッドノードに要素がある場合、ヘッド内の要素が直接ポップアップされ、ノードによる要素への参照がクリアされます。ヘッドノードの要素が空の場合、ヘッドノードが更新されます。

一般的な理解があれば、ソースコードを読んで分析できます。

// 从队头出队
public E poll() {
    
    
    restartFromHead:
    for (;;) {
    
    
        // p 表示头节点,需要出队的节点
        for (Node<E> h = head, p = h, q;;) {
    
    
            // 获取p节点的元素
            E item = p.item;
            // 如果头节点p中元素不为空,则进行CAS清空p节点对元素的应用,返回p节点的元素
            if (item != null && p.casItem(item, null)) {
    
    
                // CAS 设为成功后进入到这里,需要判断头节点p是否和head节点不是同一个了,即头节点p已经变更了
                if (p != h) // hop two nodes at a time
                    // 更新head节点,将p节点的next节点设为head节点,如果p.next不为空则设置p.next,否则设置p本身
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            // 到这说明头节点p中的元素为null或者发生冲突CAS失败,CAS失败也说明被别的线程取走了当前元素,所以就该取一个节点即next节点
            // 如果队列头节点p的next节点为空,说明队列已空,将则head设为p,返回null
            else if ((q = p.next) == null) {
    
    
                updateHead(h, p);
                return null;
            }
            // 进到这里说明 (q = p.next) == null 返回false,即p的next不为空
            // 同时q=p.next,而这个分支是说p=p.next,只有队列初始化时满足条件,两者都为空,则返回head,重头开始赋值
            else if (p == q)
                continue restartFromHead;
            else
                // 说明p节点的next不为空,且队列不是初始化状态,所以头节点p指向p.next
                p = q;
        }
    }
}

最初にヘッドノードの要素を取得し、次にヘッドノードの要素が空かどうかを判断します。空の場合は、別のスレッドがデキュー操作を実行してノードの要素を削除したことを意味します。空でない場合は、CASを使用して削除しますヘッドノードの参照はnullに設定されます。CASが成功した場合、ヘッドノードの要素が直接返されます。失敗した場合、別のスレッドがデキュー操作を実行し、ヘッドノードを更新したため、要素が変更され、ヘッドを再度取得する必要があります。ノード。

デキュー操作では、ヘッドノードがヘッドノードpと等しくない場合、ヘッドノードがヘッドノードpと等しくない場合にのみ、ヘッドがチームの最新のヘッドノードに設定されます。これにより、CAS操作が削減され、効率が向上します。

総括する

  1. 構造はリンクリストで構成されているため、ConcurrentLinkedQueueは制限されていません。リンクリストは本質的に制限がなく、システムリソースのサイズによって制限されます。
  2. ConcurrentLinkedQueueは、キューに出入りするときにCAS更新の先頭と末尾を削減する操作を採用して、パフォーマンスを向上させます。
  3. ConcurrentLinkedQueueは非ブロッキングモードで実装されます。つまり、ロックなしで、スピンとCASによってスレッドの安全性が実現されます。

今日学んだコンカレントパッケージのスレッドセーフな無制限キューのConcurrentLinkedQueueソースコードは難しくありません。これにより、理解が深まり、将来の仕事やインタビューでの使用が容易になると思います。

著者のレベルには限りがあり、記事に間違いがあるのは間違いありませんが、間違いがあれば是非議論して議論してください。私はここでそれをすべて見ました、コードワードは簡単ではなく、かわいいです、あなたは「好き」であることを覚えています

(全文の終わり)戦い!

個人公開口座

ここに画像の説明を挿入

  • 彼らが上手に書いていると感じている友人は気に入ることができて、フォローすることができます
  • 記事が正しくない場合は、指摘してください。読んでいただきありがとうございます。
  • 私は公式アカウントに注意を払うことをお勧めします。私は定期的にオリジナルの乾物記事をプッシュし、あなたを高品質の学習コミュニティに引き込みます。
  • githubアドレス:github.com/coderluojust/qige_blogs

おすすめ

転載: blog.csdn.net/taurus_7c/article/details/106075750