HashMapとConcurrentHashMapの主なポイント

1. HashMap

HashMapの基礎となるデータ構造

JDK7:配列+リンクリスト

JDK8:配列+リンクリスト+赤黒木(ソースコードを見た人は、JDK8が一方向リンクリストと双方向リンクリストの両方を使用していることを知っているはずです。双方向リンクリストは、主にリンクリストの便利な操作のためのものです。挿入、展開すると、リンクリストが赤に変わりますブラックツリーとレッドブラックツリーは、リンクリストに転送する過程でリンクリストを操作する必要があります)

JDK8のHashMapに赤と黒のツリーを使用する理由

要素の数がしきい値よりも少ない場合、リンクリストの挿入クエリ全体の効率は赤黒ツリーよりも高くなります。要素の数がこのしきい値よりも大きい場合、リンクリスト全体の挿入クエリ効率は赤黒ツリーよりも低くなります。このしきい値はHashMapでは8です。

JDK8のHashMapはリンクされたリストを赤黒木にいつ変換しますか?

ほとんどの答えは次のとおりです。リンクリストの要素数が8を超えると、リンクリストが赤黒ツリーに変換されます。

しかし、実際には別の制限があります。リンクされたリストの要素数が8より大きいことが判明した場合、現在の配列の長さが判断されます。配列の長さが64未満の場合、現時点では赤黒ツリーに変換されませんが、拡張。リンクリストの要素数が8より大きく、配列の長さが64以上の場合にのみ、リンクリストは赤黒ツリーに変換されます

上記の拡張の理由は、配列の長さがそれでも比較的小さい場合は、最初に拡張を使用してリンクリストの長さを短くするためです。

JDK8でHashMapのputメソッドを実装する方法は?

  1. キーに従ってハッシュコードを生成する
  2. 現在のHashMapオブジェクトの配列が空かどうかを判別します。空の場合は、配列を初期化します
  3. 論理AND演算に従って、ハッシュコードは現在の配列に対応する配列インデックスiに基づいて計算されます
  4. 配列のi番目の要素(タブ[i])が空かどうかを判別
  • a。空の場合、キーと値はNodeオブジェクトとしてカプセル化され、タブ[i]に割り当てられます
  • b。空でない場合:
    • putメソッドによって渡されたキーがtab [i] .keyと等しい場合、同じキーが存在することを証明します
    • それがタブ[i] .keyと等しくない場合、次のようになります。
      • タブ[i]のタイプがTreeNodeの場合、これは配列のi番目の位置が赤黒ツリーであることを意味し、キーと値を赤黒ツリーに挿入し、挿入前に赤黒ツリーに存在するかどうかを判断します同じキー
      • タブ[i]のタイプがTreeNodeでない場合は、配列のi番目の位置がリンクリストであることを意味し、リンクリストを反復処理して同じキーが存在するかどうかを確認し、トラバースプロセス中にリンクリストのノード数をカウントします。最後のノードに到達すると、キーと値がノードとしてカプセル化され、リンクリストの末尾に挿入されます。同時に、新しいノードを挿入する前のリンクリストノードの数が8以上かどうかが判断されます。そうである場合、リンクリストは赤に変わります黒い木。
    • 上記の手順で同じキーが見つかった場合、onlyIfAbsentフラグに従って値を更新する必要があるかどうかを判断し、oldValueを返します。
  1. modCount ++
  2. HashMap要素番号のサイズに1を加えたもの
  3. サイズが拡張しきい値より大きい場合、容量を拡張します

JDK8でのHashMapのgetメソッドの実装プロセス

  1. キーに従ってハッシュコードを生成する
  2. 配列が空の場合、空を直接返します
  3. 配列が空でない場合は、ハッシュコードと配列の長さを使用して、論理AND演算によってキーに対応する配列インデックスiを計算します
  4. 配列のi番目の位置に要素がない場合は、空を直接返します
  5. 配列の最初のビットの要素のキーがgetメソッドによって渡されたキーと等しい場合、要素を返し、要素の値を取得します
  6. 等しくない場合は、この要素に次の要素があるかどうかを判断し、ない場合は空を返す
  7. もしそうなら、要素のタイプがリンクリストノードか赤黒ツリーノードかを決定します
  • a。リンクリストの場合は、リンクリストをたどります
  • b。赤黒木である場合は、赤黒木をトラバースする
  1. 見つかった場合は要素を返し、見つからない場合は空にします

JDK7とJDK8のHashMapの違い

  1. JDK8では赤と黒の木が使用されています
  2. JDK7でのリンクリストの挿入に使用されるヘッド挿入方法(ヘッド挿入方法は、転送要素を展開するときにも使用されます。ヘッド挿入方法はより高速であり、リンクリストを走査する必要はありませんが、マルチスレッド拡張の場合、循環リンクリストが表示されます。問題、CPUが急上昇する)、JDK8のリンクリストで使用されているテール補間法(JDK8はリンクリストの現在のノード数を計算するため、リンクリストを走査するため、直接テール補間法を使用します)
  3. JDK7のハッシュアルゴリズムはJDK8のハッシュアルゴリズムよりも複雑です。ハッシュアルゴリズムが複雑になるほど、生成されるハッシュコードが多くなり、ハッシュマップ内の要素のハッシュが増加します。ハッシュマップのハッシュが多いほど、クエリのパフォーマンスが向上します。ツリー、つまりハッシュアルゴリズムを最適化して要素をよりハッシュ化することだけが可能であり、JDK8は赤と黒のツリーを追加するため、クエリのパフォーマンスが保証され、ハッシュアルゴリズムを簡略化できます。結局のところ、ハッシュアルゴリズムが複雑になるほど、CPUの消費量も増えます。
  4. 容量拡張の過程で、JDK7はキーを再ハッシュする可能性があります(再ハッシュはハッシュシードに関連しています)が、JDK8にはロジックのこの部分がありません
  5. JDK8の容量拡張の条件はJDK7の条件とは異なります。JDK7は、サイズがしきい値より大きいかどうかを判断するだけでなく、タブ[i]が空かどうかも判断し、空でないときに拡張を実行しますが、JDK8にはこの条件がありません。オフ
  6. JDK8にはもう1つのAPIがあります。putIfAbsent(key、value)
  7. JDK7とJDK8は、拡張プロセス中に要素を転送するロジックが異なります。JDK7は、一度に1つの要素を転送します。JDK8は、現在の位置のどの要素が新しい配列の低い位置にあり、どの要素が新しい配列の高い位置にあるかを計算し、次に一度に計算します。転送

第二に、ConcurrentHashMap

JDK7のConcurrentHashMapはどのように並行性のセキュリティを保証しますか?

安全でない操作+ ReentrantLock +セグメンテーションのアイデアを主に使用します。安全でない操作で主に使用されます:

  1. compareAndSwapObject:casを介してオブジェクトのプロパティを変更します
  2. putOrderedObject:配列の位置に同時に安全に値を割り当てます
  3. getObjectVolatile:配列内の特定の位置の要素を同時に安全に取得します

セグメント化の目的は、ConcurrentHashMapの同時量を改善することです。セグメント数が多いほど、サポートされる最大同時量が多くなります。プログラマーは、concurrencyLevelパラメーターを使用して同時量を指定できます。ConcurrentHashMapの内部クラスSegmentは、特定のセグメントを表すために使用されます。

各セグメントは小さなHashMapです。ConcurrentHashMapのputメソッドが呼び出されると、最終的にセグメントのputメソッドが呼び出され、SegmentクラスはReentrantLockを継承するため、セグメントには再入可能ロックが付属します。ロックに再度入ると、挿入されるキーと値は、ロックが成功した後に小さなHashMapに挿入され、挿入が完了した後にロックが解除されます。

JDK7のConcurrentHashMapの基本原理

ConcurrentHashMapの最下層は、ネストされた配列の2つの層によって実装されます。

  1. タイプSegment []のConcurrentHashMapオブジェクトには属性セグメントがあります。
  2. Segmentオブジェクトには、HashEntry []タイプの属性テーブルがあります。

ConcurrentHashMapのputメソッドを呼び出すときは、まずキーに従って対応するセグメント[]の配列インデックスjを計算し、現在のキーと値を挿入する必要があるセグメントオブジェクトを決定します。セグメント[j]が空の場合は、スピンを使用しますlockメソッドは、j位置にSegmentオブジェクトを生成します。

次に、Segmentオブジェクトのputメソッドを呼び出します。

Segmentオブジェクトのputメソッドは最初にロックし、次にキーに従ってHashEntry []配列の対応する添え字iを計算し、次にキーと値をHashEntryオブジェクトのこの場所にカプセル化します。このプロセスは、JDK7のHashMapのputメソッドです同じように、ロックを解除します。

ロックのプロセスでは、ロジックがより複雑になります。まず、スピンを使用してロックし、一定の回数を超えると、直接ブロックするなどします。

JDK8のConcurrentHashMapはどのように並行性のセキュリティを保証しますか?

主にUnsafe operation + synchronizedキーワードを使用します。

Unsafe操作の使用は、依然としてJDK7の場合と同様であり、主に、オブジェクトのプロパティまたは配列内の位置の値を同時に安全に変更する責任があります。

Synchronizedは、特定の位置にあるリンクリストにノードを挿入したり、特定の位置にある赤と黒のツリーにノードを挿入したりするなど、位置を操作する必要がある場合(位置が空でない場合)のロックを主に担当します。

実際、JDK8にはまだセグメント化されたロックの概念がありますが、JDK7のセグメント数は制御でき、JDK8では配列の各位置にロックがあります。キー、値をConcurrentHashMapに入れると、

  1. まず、対応する配列のインデックスiをキーに従って計算し、この位置に要素がない場合は、スピンによってこの位置に値を割り当てます。
  2. この位置に要素がある場合、同期はロックされます
  3. ロックに成功したら、要素のタイプを判別します
  • a。リンクリストノードの場合は、リンクリストにノードを追加します
  • b。赤黒ツリーの場合は、赤黒ツリーにノードを追加します
  1. 追加が成功したら、ツリー化が必要かどうかを判断します
  2. addCount、このメソッドは、ConcurrentHashMapの要素の数が1増加することを意味しますが、この操作も同時に安全である必要があり、要素の数が1増加した後、必要に応じて容量を拡張するかどうかを判断し続けます。この方法は非常に重要です。
  3. 同時に、スレッドが現在のConcurrentHashMapが拡張していることを検出した場合、拡張を支援します。

JDK7とJDK8のConcurrentHashMapの違い

2つの...の違いが多すぎます。これには、HashMapの違いと、次のような他の違いの両方が含まれます。

  1. JDK8にはセグメントロックはありませんが、同期化を使用して制御します
  2. JDK8の拡張パフォーマンスはより高く、同時にマルチスレッド拡張をサポートします。実際、JDK7はマルチスレッド拡張もサポートします。JDK7での拡張はセグメントごとであるため、マルチスレッド拡張も可能ですが、パフォーマンスがJDK8ほど高くありません。 JDK8のどのスレッドも容量の拡張に役立ちます
  3. JDK8内の要素数の実装も異なります。CounterCellは、JDK8では追加されますが、JDK7では追加されません。JDK7では、各セグメントは、配置時に内部的にカウントされ、統計は各セグメントオブジェクトとロック統計

おすすめ

転載: juejin.im/post/5e9301f9518825738b42171c