Java 8はConcurrentHashMapを開始し、なぜセグメントロックを捨てるのか

3463 ワード

概要


Java 5の後、JDKはjavaを導入したことを知っています.util.concurrentは発注して、その中で最もよく使うのはConcurrentHashMapで、その原理は内部のSegment(ReentrantLock)セグメントロックを引用して、異なるセグメントmapを操作する時、同時に実行することができて、同じセグメントmapを操作する時、ロックの競争と待つことを保証します.これにより、スレッドが安全になり、synchronizedよりも効率が向上します.
しかしJava 8の後、JDKはこのポリシーを破棄しsynchronized+casを再利用した.

廃棄理由


JDKのソースコードと公式ドキュメントから、セグメントロックを破棄した理由は以下の点にあると考えられています.
  • は、複数のセグメントロックを追加してメモリ領域を浪費します.
  • の生産環境ではmapが同じロックを入れる際に競合する確率は非常に小さく,セグメントロックは逆に更新などの操作を長時間待つことになる.
  • GCの効率向上のため
  • 新しい同期スキーム


    セグメントロックが廃止された以上、新しいスレッドセキュリティスキームによって、ソースコードがスレッドセキュリティをどのように解決しているかを見てみましょう.(ソースコードはsegmentコードを保持しているが、使用されていない)
    put
    まずhashで対応するチェーンテーブルを見つけた後、最初のobjectであるかどうかを確認し、もしそうであればcas原則で直接挿入し、ロックをかける必要はありません.
    Node f; int n, i, fh; K fk; V fv;
    if (tab == null || (n = tab.length) == 0)
        tab = initTable(); //  map , hash ,  table
    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    // object,  cas ,  
        if (casTabAt(tab, i, null, new Node(hash, key, value)))
            break;                   
    }

    次に、チェーンテーブルの最初のobjectでない場合は、チェーンテーブルの最初のobjectで直接ロックします.ここでロックするのはsynchronizedです.効率はReentrantLockに及ばないが、スペースを節約します.ここでは、mapサイズを再計算したり、最初のobjectを操作したりするまで、最初のobjectでロックします.
    synchronized (f) {//  f object
        if (tabAt(tab, i) == f) {
            if (fh >= 0) {
                binCount = 1;
                for (Node e = f;; ++binCount) {
                    K ek;
                    if (e.hash == hash &&
                        ((ek = e.key) == key ||
                         (ek != null && key.equals(ek)))) {
                        oldVal = e.val;
                        if (!onlyIfAbsent)
                            e.val = value;
                        break;
                    }
                    Node pred = e;
                    if ((e = e.next) == null) {
                        pred.next = new Node(hash, key, value);
                        break;
                    }
                }
            }
            else if (f instanceof TreeBin) { //  
                Node p;
                binCount = 2;
                if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                               value)) != null) {
                    oldVal = p.val;
                    if (!onlyIfAbsent)
                        p.val = value;
                }
            }
            else if (f instanceof ReservationNode)
                throw new IllegalStateException("Recursive update");
        }
    }

    セグメントロックテクノロジーはjava 8以前に使用され、java 8では廃棄されsynchronized+casに更新されました.
     
     
    ConcurrentHashMap(JDK 1.8)では、ReentranLockのような再ロックではなくsynchronizedを使用するのはなぜですか?
    この問題について、次のいくつかの角度から議論したいと思います.
    ロックの粒度はまずロックの粒度が太くなく、さらに細くなった.一度拡張するたびに,ConcurrentHashMapの同時性は2倍に拡大した.Hash衝突JDK 1.7では、ConcurrentHashMapは、2回目のhash方式(Segment->HashEntry)から、検索された要素をすばやく見つけることができます.1.8ではチェーンテーブルに赤黒ツリーを付ける形でput,get時の性能差を補った.拡張JDK 1.8では,ConcurrentHashmapが拡張する場合,他のスレッドは配列中のノードを検出することによって,このチェーンテーブル(赤黒木)を拡張するか否かを決定し,拡張の粒度を低減し,拡張の効率を向上させることができる.次は面接中の質問に対する私の見方です.
    なぜReentranLockではなくsynchronizedなのか.メモリオーバーヘッドを低減するには、再ロックを使用して同期サポートを取得すると仮定し、各ノードはAQSを継承することによって同期サポートを取得する必要がある.しかし、各ノードが同期サポートを必要とするわけではありません.チェーンテーブルのヘッダノード(赤と黒のツリーのルートノード)だけが同期を必要とします.これは大きなメモリの浪費をもたらすに違いありません.2.JVMのサポートを獲得する再ロックは結局APIというレベルであり、後続の性能最適化空間は小さい.synchronizedはJVMが直接サポートしており、JVMは実行時にロック粗化、ロック除去、ロックスピンなどの最適化措置を講じることができます.これによりsynchronizedは、コードを変更せずにJDKバージョンのアップグレードに伴ってパフォーマンスの向上を得ることができます.