HashMap の構造と基本原則:
HashMap は非常に一般的に使用されるデータ構造であり、配列と連結リストで構成されるデータ構造です 。
おおよそ次のように、配列内の各場所に Key-Value のインスタンスが格納されます。これは、Java7 では Entry と呼ばれ、Java8 では Node と呼ばれます。
すべての位置が null であるため、プットが挿入されると、キーのハッシュ アルゴリズムに従ってインデックス値が計算されます。たとえば、("Shuaibing", 220) と入力すると、"Hello" という名前の要素が挿入されます. このとき、挿入された位置のインデックスをハッシュ アルゴリズムで計算すると、計算されたインデックスは 2 です. 結果は次のようになります。次のとおりです。
ハッシュ (「ハンサム」) = 2
先ほど、連結リストもあると言いましたが、なぜ連結リストが必要なのですか? また、連結リストはどのようなものですか?
配列の長さが制限されていることはわかっています.制限された長さでは、ハッシュを使用し、ハッシュ自体は確率論的です.つまり、Shuai BとB Shuaiのハッシュ値は一定の確率で同じになります:
ハッシュ("ビンシュアイ") = 2
次に、次のようなリンクされたリストが形成されます。
各ノード ノードは、独自のハッシュ、キー、値、および次のノード ノードを保存します。ノードのソース コードは次のとおりです。
連結リストといえば、連結リストに新しいエントリーノード(Node)を挿入する際、どのように挿入されるかご存知ですか?
Java 8 より前は、ヘッダー挿入メソッドが使用されていました。これは、新しい値がヘッダーの元の値を置き換え、元の値が上記の例のようにリンクされたリストにプッシュされることを意味します。最新の値は後ほど差し替えます 検索の可能性が少し増えますし、検索効率を上げるためでもあります。
しかし、java8以降は末尾挿入に変更されました。
では、なぜテール挿入に変更するのでしょうか。
まず、 HashMapの拡張メカニズムを見てみましょう:
先述の通り、配列の容量には限りがあり、何度もデータを挿入し、一定量に達したら拡張、つまりリサイズします。
リサイズ展開のタイミングは?
次の 2 つの要因があります。
- Capacity : HashMap の現在の長さ。
- LoadFactor : 負荷係数、デフォルト値は 0.75f です。
例えば、現在のアレイ容量が100の場合、76個目のアレイを保存すると、LoadFactorの規定値に達したと判断されます.このとき、リサイズが必要なので、容量を拡張してください.ですが、HashMap の容量拡張は単純に容量を拡張するだけではありません。
拡張?それはどのように拡大しますか?
2 つのステップに分かれています。
- 拡張: 元の配列の2 倍の長さの新しい Entry 空の配列を作成します。
- ReHash : 元の Entry 配列をトラバースし、すべての Entry を新しい配列に再ハッシュします。
なぜ再ハッシュしたいのか、過去をそのままコピーした方がいいのではないか?
これは、新しい Entry 配列の長さが拡張された後、それに応じて Hash のルールも変更されるためです。
ハッシュ式: インデックス = HashCode (キー) & (長さ - 1)
元の配列の長さ (Length) は 8、ビット操作によって得られる値は 2、新しい配列の長さは 16 であり、ビット操作によって得られる値は明らかに異なります。
拡張前:
拡張後:
展開の仕組みについて話した後で、上記の質問に答えましょう。以前は head 挿入方式を使用していたのに、java8 以降は tail 挿入方式に変更したのはなぜですか?
例えば:
ここで、容量 2 のコンテナーに A、B、C を別のスレッドで挿入したいと考えています。サイズ変更前にブレークポイントを設定すると、データは挿入されていますが、サイズ変更は実行されていないため、それは拡張の前かもしれません
リンクされたリストが A->B->C を指していることがわかります。
単一連結リストの先頭挿入方法を使用すると、同じ位置にある新しい要素は常に連結リストの先頭に配置されます。古い配列の同じ Entry チェーンの要素は、インデックス位置を再計算した後、新しい配列の別の位置に配置される場合があります。
B の次のポインターが A を指している可能性があります。
いくつかのスレッドが調整されると、循環リンク リストが表示される場合があります。
この時点で値を取得しに行くと、悲劇が発生します-無限ループ。
上記の例は、1.7以前のヘッド挿入方法を説明するためのものですが、並行シナリオでは、データループが形成される可能性があり、データ取得時に無限ループが発生するという致命的な問題があります。( HashMap はスレッドセーフではありませんが)
1.8 より前では、ハッシュの競合に対処する方法は、リンクされたリストを使用して解決するデータを格納し、ヘッド挿入メソッドを使用して特定の効率を向上させることでした。
しかし1.8以降はこの効率化は必須ではなく、連結リストの長さが7を超えるため、赤黒木のアップグレードを検討する必要があるため、テール挿入トラバーサルの数が制限されていても、効率は大幅に向上しません。影響を受ける。
次に、1.8 以降のデータ構造の変更により、リンクされたリストの長さがしきい値に達すると、赤黒ツリーの構築にシーケンスを比較して更新するため、ヘッド補間法とは言えません。
ヘッダー挿入を使用すると連結リストの順序が変更されますが、末尾挿入を使用すると、拡張時に連結リスト要素の元の順序が維持され、連結リストがループを形成する問題が発生しなくなります。
つまり、元は A->B でしたが、連結リストは展開後も A->B のままです。
Java 1.7 では HashMap を複数のスレッドで操作すると無限ループが発生する場合があります. これは展開転送後に前後の連結リストの順序が逆になり, 元の連結リストのノードの参照関係が途中で変更されるためです.転送プロセス。
同じ前提で、Java 1.8 では無限ループが発生しない理由は、拡張転送後も連結リストの順序が変わらず、以前のノードの参照関係が維持されるためです。
それは、Java8 がマルチスレッドで HashMap を使用できるということですか?
たとえ無限ループが発生しなくても、ソースコードを見ると、put/get メソッドに同期ロックがないことがわかります。最後の 1 秒間の put の値は保証されず、次の 1 秒間の get の値は同じままであるため、スレッドの安全性はまだ保証されていません。
では、HashMap のデフォルトの初期化の長さは?
ソースコードを見たときの初期サイズは16だった記憶があります。
では、なぜ16なのですか?
JDK1.8 の 236 行目には 1<<4 があり、これは 16 です。
ビット操作を使用する理由 16を直接使うのはよくないですか?
主な理由は,ビット操作のパフォーマンスが良い,ビットと操作の効率が算術計算のパフォーマンスよりもはるかに高い.なぜビット操作のパフォーマンスがとても良いのですか? それはビット操作が直接メモリ上で動作し、バイナリ変換を実行する必要はありません. コンピュータはバイナリをデータストレージの形で使用することを知っておく必要があります.
では、なぜ他の数字の代わりに 16 を使うのでしょうか?
なぜ 16 なのかを知るには、HashMap のデータ ストレージの特性を調べる必要があります。
たとえば、キーが「Shuaibing」の場合、10 進値は 766132、2 進値は 10111011000010110100 です。
指数の計算式をもう一度見てみましょう。
Hash公式: index = HashCode(Key) & (Length- 1)
したがって、インデックス = 1011101100001011 0100 & (16 -1);
したがって、インデックス = 1011101100001011 0100 & 15;
15 は 2 進数で 1111 であり、次のようになります。
インデックス = 1011101100001011 0100 & 1111 = 0100 = 4。
ビット AND 演算を使用する理由は、効果が modulo と同じであり、パフォーマンスが大幅に向上するためです。
Hash アルゴリズムによって得られる最終的なインデックス結果は、 Key の Hashcode 値(バイナリ)の最後の数桁に完全に依存していると言えます。
また、 初期サイズとして 2 のべき乗の数値を使用する場合、(Length-1) の値のバイナリ ビットはすべて 1 であるため、この場合、index の結果は HashCode の最後の数桁に相当します (バイナリ) 値。
したがって、入力 HashCode 自体が均等に分散されていれば、均等に分散されたハッシュが得られる、つまり、ハッシュの競合を最小限に抑えることができます。
では、なぜ16なのですか?2の整数乗じゃないの?理論上は大丈夫なのですが、2、4、8だとちょっと小さいかな、あまりデータを追加せずに容量拡張、つまり頻繁に容量拡張して性能に影響します. なぜ 32 以上にしないのですか? スペースの無駄ではないので、16 は非常に適切な経験値として予約されています。
もう 1 つの質問ですが、equals メソッドを書き直すときに、hashCode メソッドを書き直す必要があるのはなぜですか?
HashMap を使用した例を教えてください。
Java では、すべてのオブジェクトが Object クラスから継承されるためです。Ojbect クラスにはequalsとhashCode の2 つのメソッドがあり、どちらも 2 つのオブジェクトが等しいかどうかを比較するために使用されます。
equals メソッドが書き換えられていない場合, オブジェクトの equals のデフォルトの実装を継承します. 内部の equals は 2 つのオブジェクトのメモリアドレスを比較するためのものです. 明らかに, 2 つの新しいオブジェクトを作成したので, それらのメモリアドレスは異なっている必要があります.
- 値オブジェクトの場合、== は 2 つのオブジェクトの値を比較します
- 参照オブジェクトの場合、2 つのオブジェクトのメモリ アドレスが比較されます。
ハッシュコードの特徴は次のとおりです。
同じオブジェクトの場合、変更されていない場合 (equals を使用して true を返す)、そのハッシュコード値はいつでも同じです。
2 つのオブジェクトについて、equals が false を返す場合、それらのハッシュコード値も等しい可能性があります
HashMap にデータを追加する場合、次の 2 つの状況があります。
- 現在の配列インデックスが空の場所。この状況は非常に単純で、要素を直接配置するだけです。
- インデックスの位置に既に要素が存在する場合、その位置にある要素が現在の要素と等しいかどうかを判定し、equals を使用して比較する必要があります。
デフォルトのルールを使用すると、2 つのオブジェクトのアドレスが比較されます。つまり、2 つが等しいためには、同じオブジェクトである必要があります. もちろん、equals メソッドを書き換えて、独自の比較ルールを実装することもできます. 最も一般的なのは、属性値を比較して等しいかどうかを判断することです.
2 つが等しい場合は直接上書きされ、等しくない場合は元の要素の下の連結リストの構造に要素が格納されます。
では、カスタム オブジェクトをキーとして使用する場合、equals メソッドを書き換えながら hashCode メソッドを書き換える必要があるのはなぜでしょうか。
ハッシュコードを書き換えずに equals を書き換えると、どのような問題が発生するか( Hashmap の観点から)例を見てみましょう。
public class MyTest {
private static class Person{
int idCard;
String name;
public Person(int idCard, String name) {
this.idCard = idCard;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()){
return false;
}
Person person = (Person) o;
//两个对象是否等值,通过idCard来确定
return this.idCard == person.idCard;
}
}
public static void main(String []args){
HashMap<Person,String> map = new HashMap<Person, String>();
Person person = new Person(1234,"乔峰");
//put到hashmap中去
map.put(person,"天龙八部");
//get取出,从逻辑上讲应该能输出“天龙八部”
System.out.println("结果:"+map.get(new Person(1234,"萧峰")));
}
}
实际输出结果:null
HashMap の原理をある程度理解していれば、この結果を理解するのは難しくありません。get 操作と put 操作を実行する場合、使用するキーは論理的に等価 (equals 比較を通じて等しい) ですが、hashCode メソッドは書き直されていないため、put 操作が実行されると、key(hashcode1)–> hash–>indexFor–>final index position、および key(hashcode2)–>hash–>indexFor–>final インデックス位置は、キーを介して値を抽出するときに、hashcode1 が hashcode2 と等しくないため、配列の位置を特定せず、論理的に返されます。正しくない値は null です。
したがって、equals メソッドを書き換える場合は、hashCode メソッドの書き換えに注意する必要がありますが、同時に、equals で等しいと判断された 2 つのオブジェクトが、hashCode メソッドを呼び出したときに同じ整数値を返すようにする必要があります。
jdk1.8のHashMapの連結リストが赤黒木になるのはいつですか?
Hashmap の連結リストのサイズが 8 を超えると自動的に赤黒ツリーに変換され、削除が 6 未満になると再び連結リストになります。
もう 1 つの質問ですが、HashMap がスレッドセーフでない場合、スレッドセーフなシナリオで HashMap をどのように扱うかについて教えていただけますか?
そのようなシナリオでは、通常はHashTableまたはConcurrentHashMapを使用しますが、前者の同時実行性のため、基本的に使用シナリオはなく、スレッドが安全でないシナリオではすべて ConcurrentHashMap を使用します。HashTable のソース コードを読みました。非常に単純で失礼です。メソッドで直接ロックされています。同時実行性は非常に低く、同時にアクセスできるスレッドは最大で 1 つです。ConcurrentHashMap の方がはるかに優れています。 1.7 と 1.8 の大きな違いですが、同時実行性は前者の方がはるかに優れています。
Set の観点から equals を書き換えるには、hashCode を書き換える必要があります
ConcurrentHashMap について教えてください。
ConcurrentHashMap は Hashtable と比較して同時実行性が高いため、一般的なマルチスレッド操作では、基本的に ConcurrentHashMap が選択されます。
最初に Hashtable について話しましょうか。
HashMap と比較すると、Hashtable はスレッドセーフであり、マルチスレッド環境での使用に適していますが、効率は楽観的ではありません。
HashTable の効率の低さについて教えてください。
ソース コードでは、データを操作するときにロックされるため、効率は比較的低くなります。
これ以外に、Hashtable と HashMap に違いはありますか?
Hashtable ではキーまたは値を null にすることはできませんが、HashMap のキー値は null にすることができます。
Hashtable では KEY と VALUE を null にできないのに、HashMap ではできるのはなぜですか?
null 値を入れると、Hashtable は直接 null ポインター例外をスローしますが、HashMap は特別な処理を行っているためです。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
Hashtable がキーまたは値を null にすることを許可しない理由はまだ明らかではないようですが、HashMap のキー値は null にすることができますか?
これは、Hashtable がフェールセーフ機構 (fail-safe) を使用しているため、今回読み取ったデータが必ずしも最新のデータであるとは限りません。
null 値を使用すると、再度 contains(key) を呼び出してキーが存在するかどうかを判断できないため、対応するキーが存在しないか空であるかを判断できなくなります. ConcurrentHashMap も同様です.
違いはありますか?
-
実装が異なります。Hashtable は Dictionary クラスを継承し、HashMap は AbstractMap クラスを継承します。
誰もこの辞書を使っていないようですし、私も使っていません。
-
初期容量が異なります。HashMap の初期容量は16、Hashtable の初期容量は11、両方のデフォルトの負荷係数は 0.75 です。
-
拡張メカニズムが異なります。既存の容量が総容量 * 負荷係数より大きい場合、HashMap 拡張ルールは現在の容量の 2 倍になり、Hashtable 拡張ルールは現在の容量の 2 倍 + 1 になります。
-
イテレータは異なります: HashMap のイテレータ イテレータはフェイル ファストですが、Hashtable の列挙子はフェイル ファストではありません。
したがって、要素の追加や削除など、他のスレッドが HashMap の構造を変更すると、ConcurrentModificationException がスローされますが、Hashtable はスローされません。
フェイルファストとは?
高速障害 (fail—fast)は、Java コレクションのメカニズムです。反復子を使用してコレクション オブジェクトをトラバースする場合、トラバース プロセス中にコレクション オブジェクトのコンテンツが変更 (追加、削除、変更) されると、同時変更例外がスローされます。例外。
フェイルファストの原理とは?
反復子は、トラバース中にコレクションの内容に直接アクセスし、トラバース中に modCount 変数を使用します。
トラバーサル中にコレクションの内容が変更されると、modCountの値が変更されます。
イテレータが次の要素をトラバースする前に hashNext() / next() を使用するときはいつでも、modCount 変数が期待される modCount 値であるかどうかをチェックし、そうである場合はトラバーサルに戻ります; そうでない場合は例外がスローされ、トラバーサルは終了します.
ヒント: ここでの例外スロー条件は、modCount != expectedmodCount という条件を検出することです。コレクションが変更されたときに modCount 値が変更され、予想される modCount 値に設定された場合、例外はスローされません。
したがって、この例外がスローされるかどうかに応じて、同時操作をプログラムすることはできません。この例外は、同時変更のバグを検出する目的でのみ推奨されます。
フェイルファストのシーンについて教えてください。
java.util パッケージのコレクション クラスはすべて高速障害メカニズムに基づいており、セキュリティ メカニズムと見なされるマルチスレッド (反復プロセス中に変更) で同時に変更することはできません。
ヒント:安全性の失敗 (fail—safe) java.util.concurrent パッケージのコンテナーは安全な失敗であり、マルチスレッドで同時に使用および変更できることも理解できます。
次に、ConcurrentHashMap について話しましょうか。
そのデータ構造について話しましょう。なぜその同時実行性が非常に高いのでしょうか?
ConcurrentHashMap の最下層は 数组 + 链表
構成に基づいていますが、具体的な実装は jdk1.7 と 1.8 で若干異なります。最初に 1.7 での彼のデータ構造について話しましょう。
上の図に示すように、ConcurrentHashMap は Segment 配列と HashEntry で構成されています. HashMap と同様に、配列に連結リストを加えたものです。
セグメントは ConcurrentHashMap の内部クラスであり、主なコンポーネントは次のとおりです。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
// 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
transient volatile HashEntry<K,V>[] table;
transient int count; // 记得快速失败(fail—fast)么?
transient int modCount; // 大小
transient int threshold; // 负载因子
final float loadFactor;
}
HashEntry は HashMap に似ていますが、違いは、HashEntry が volatile を使用してそのデータ値と次のノードを変更することです。
揮発性の特徴は何ですか?
-
これにより、この変数で動作するさまざまなスレッドの可視性が保証されます。つまり、スレッドが変数の値を変更すると、この新しい値が他のスレッドにすぐに表示されます。(可視性のため)
-
命令の並べ替えは禁止されています。(注文を達成するため)
-
volatile は、単一の読み取り/書き込みの原子性のみを保証できます。i++ は、このような操作の原子性を保証しません。
あまり多くのスペースを導入することはしません。マルチスレッドの章について話します。使用後は安全であることは誰もが知っています。
彼の同時実行率が高い理由を教えてください。
原則として、ConcurrentHashMap は、Segment が ReentrantLock を継承するセグメント ロックテクノロジを使用します。
HashTable とは異なり、put 操作と get 操作の両方を同期する必要があります (理論上、ConcurrentHashMap はセグメント配列の数のスレッド同時実行をサポートします)。スレッドがセグメントにアクセスするためにロックを占有しても、他のセグメントには影響しません。つまり、容量が 16 の場合、同時実行性は 16 であり、16 のスレッドが同時に 16 のセグメントを操作でき、スレッドセーフです。
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();//这就是为啥他不可以put null值的原因
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
最初にセグメントを見つけてから、プット操作を実行します。
彼がプットしたソースコードを見てみましょう.彼がどのようにしてスレッドセーフを達成したかがわかるでしょう.
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
} else {
// 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//释放锁
unlock();
}
return oldValue;
}
まず、最初のステップでロックの取得を試みますが、取得に失敗した場合は、他のスレッド間で競合が発生し、 scanAndLockForPut()
スピンを使用してロックを取得します。
- ロックを取得するためにスピンを試みます。
- 再試行回数に達すると
MAX_SCAN_RETRIES
、ロックの取得をブロックするように変更され、取得が成功できるようになります。
彼のgetのロジックはどうですか?
get ロジックは比較的単純で、ハッシュを介して特定のセグメントへのキーを検索し、ハッシュを介して特定の要素を 1 回検索するだけで済みます。HashEntry の value 属性は volatile キーワードで修飾されているため、メモリの可視性が保証されているため、取得するたびに最新の値になります。
プロセス全体でロックが必要ないため、ConcurrentHashMap の get メソッドは非常に効率的です。
1.7 は各セグメントへの同時アクセスをサポートできますが、まだいくつかの問題があることに気付きましたか?
はい、基本的には連結リストに配列を追加する方法なので、クエリを実行すると連結リストをたどる必要があり、効率が低下します.これはjdk1.7のHashMapと同じ問題です.ということで、完全にjdk1.8に。
彼のデータ構造は jdk1.8 ではどのようになっていますか?
その中で、元の Segment セグメントロックは放棄され、 CAS + synchronized
並行性のセキュリティを確保するために採用されています。
HashMap と非常によく似ています.これも以前の HashEntry を Node に変更しましたが,機能は同じままです.value と next は可視性を確保するために volatile で変更されます.また、赤黒ツリーも導入されます.リンクされたリストの場合特定の値より大きいものは変換されます (デフォルトは 8)。
他の値アクセス操作はどうですか? そして、スレッドの安全性を確保する方法は?
ConcurrentHashMap の put 操作はまだ比較的複雑ですが、大まかに次の手順に分けることができます。
- キーに基づいてハッシュコードを計算します。
- 初期化が必要かどうかを判別します。
- これは、現在のキーによって配置されたノードです。空の場合、現在の場所にデータを書き込むことができることを意味します。書き込みを試みるには、CAS を使用します。失敗した場合、スピンは成功を保証します。
- 現在の場所にある場合は
hashcode == MOVED == -1
、展開する必要があります。 - 満たされない場合は、同期ロックを使用してデータを書き込みます。
- 数値が
TREEIFY_THRESHOLD
それ赤黒ツリーに変換されます。
先ほど、CAS とは何ですか?スピンとは?
CAS は楽観的ロックと軽量ロックの実装です。
CAS操作の流れは下図の通りで、データ読み込み時にスレッドはロックしません。データを書き戻す準備をするときは、元の値が変更されているかどうかを比較し、他のスレッドによって変更されていない場合は書き戻され、変更されている場合は読み取りプロセスが再実行されます。
CAS は、データが他のスレッドによって変更されていないことを保証できますか?
いいえ、たとえば、古典的な ABA 問題、CAS は判断できません。
ABAとは?
つまり、あるスレッドが来て、値を B に戻し、別のスレッドが来て、値を A に戻しました。実際、多くのシーンで正しい最終結果を追求するだけであれば問題ありません。
ただし、実際のプロセスでは、資金の変更など、変更プロセスを記録する必要があります。変更するたびに記録を残して、追跡できるようにする必要があります。
では、ABA 問題を解決するにはどうすればよいでしょうか。
バージョン番号で保証するだけ. たとえば, 変更前に元の値を問い合わせる場合, バージョン番号を持ってくる. 判断するたびに, 値とバージョン番号を合わせて判断する.成功したら、バージョン番号を追加します。1.
CAS のパフォーマンスは非常に高いですが、synced のパフォーマンスが良くないことはわかっています.jdk1.8 アップグレード後、synced が増えたのはなぜですか?
Synchronized は以前は常に重量級のロックでしたが、後に Java 担当者がアップグレードし、現在はロック アップグレード メソッドを使用してそれを実行しています。
ロックを取得する同期方法については、JVM はロック アップグレードの最適化方法を使用します。これは、バイアスロックを使用して同じスレッドに優先度を与え、再度ロックを取得します。失敗した場合、軽量の CASにアップグレードされます。lock . 失敗した場合、スレッドがシステムによって中断されるのを防ぐために、短時間スピンします。最後に、上記のすべてが失敗した場合は、重いロックにアップグレードしてください。そのため、段階的にアップグレードされ、最初は多くの軽量な方法でロックされていました。
ConcurrentHashMap の get 操作はどうですか?
- 計算されたハッシュコード アドレスに従って、それがバケット上にある場合は、値を直接返します。
- 赤黒木の場合は、木に従って値を取得します。
- 満足できない場合は、トラバースして、リンクされたリストに従って値を取得します。
まとめ: 1.8 は 1.7 のデータ構造に大きな変更を加えた. 赤黒ツリーを使用した後、クエリ効率が保証されるようになり ( )、さらにO(logn)
ReentrantLock を解除して同期化に変更した. 同期化されていることがわかります.のJDKの新しいバージョンでは、最適化が行われています。