ビデオアドレス: https://www.yuque.com/linxun-bpyj0/linxun/vy91es9lyg7kbfnr
概要
基本
基礎ポイント:アルゴリズム、データ構造、基本的な設計パターン
1.二分探索
必要とする
- 二分探索アルゴリズムを自分の言語で説明できる
- 二分探索コードを手書きする機能
- 一部変更された試験方法に解答できるようにする
アルゴリズムの説明
- 前提: ソートされた配列 A があります (ソートが完了していると仮定します)。
- 左境界Lと右境界Rを定義し、探索範囲を決め、ループで二分探索を行う(ステップ3、4)
- 中間インデックスの取得 M = Floor((L+R) /2)
- 中間インデックスの値 A[M] と検索対象の値 T を比較します。
① A[M] == T は見つかったことを意味し、中間インデックスを返します。
② A[M] > T、右側の他の要素中間値は T より大きいため、比較する必要はありません。左側の中央のインデックスを見つけ、右側の境界として M - 1 を設定し、再度検索します。 ③
A[M] < T、中央値の左側の他の要素は次のとおりです。 T より小さい場合、比較する必要はありません。右側の中央のインデックスを見つけて、M + 1 を設定します。左側の境界については、再検索します。 - L > R の場合、見つからないことを意味し、ループは終了する必要があります
より具体的な説明については、binary_search.html を参照してください。
アルゴリズムの実装
public static int binarySearch(int[] a, int t) {
int l = 0, r = a.length - 1, m;
while (l <= r) {
m = (l + r) / 2;
if (a[m] == t) {
return m;
} else if (a[m] > t) {
r = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
テストコード
public static void main(String[] args) {
int[] array = {1, 5, 8, 11, 19, 22, 31, 35, 40, 45, 48, 49, 50};
int target = 47;
int idx = binarySearch(array, target);
System.out.println(idx);
}
整数オーバーフローの問題を解決する
l と r が両方とも大きい場合、l + r
整数の範囲を超えて演算エラーが発生する可能性があります。解決策は 2 つあります。
int m = l + (r - l) / 2;
もう1つは次のとおりです。
int m = (l + r) >>> 1;
その他の方法
- 1、5、8、11、19、22、31、35、40、45、48、49、50 の順序リストがあります。二分探索値 48 のノードが見つかった場合、必要な比較回数は検索が成功しました
- 二分法を使用してシーケンス 1,4,6,7,15,33,39,50,64,78,75,81,89,96 内の要素 81 を見つける場合、() 比較を行う必要があります。
- 要素 128 個の配列内の数値を二分検索する場合、必要な比較の最大回数は次の回数です。
最初の 2 つの質問については、判断のための簡単な公式を覚えておいてください。奇数の 2 点が中央を取得し、偶数 2 点が中央の左を取得します。後者の質問については、次の公式を知っておく必要があります。
ここで、n は検索数、N は要素数です。
2. バブルソート
必要とする
- バブルソートアルゴリズムを自分の言語で説明できる
- バブルソートコードを手動で作成する機能
- バブルソートのいくつかの最適化方法を理解する
アルゴリズムの説明
- 配列内の 2 つの隣接する要素のサイズを順番に比較します。a[j] > a[j+1] の場合、2 つの要素を交換します。両方の比較をバブリングのラウンドと呼びます。結果は、最大のものを配置します。要素をついに
- 配列全体がソートされるまで、上記の手順を繰り返します。
より鮮明な説明については、bubble_sort.html を参照してください。
アルゴリズムの実装
public static void bubble(int[] a) {
for (int j = 0; j < a.length - 1; j++) {
// 一轮冒泡
boolean swapped = false; // 是否发生了交换
for (int i = 0; i < a.length - 1 - j; i++) {
System.out.println("比较次数" + i);
if (a[i] > a[i + 1]) {
Utils.swap(a, i, i + 1);
swapped = true;
}
}
System.out.println("第" + j + "轮冒泡"
+ Arrays.toString(a));
if (!swapped) {
break;
}
}
}
- 最適化ポイント 1: バブリングの各ラウンドの後、内側のループを 1 回減らすことができます。
- 最適化ポイント 2: 一定のバブリングラウンドでやり取りがなければ、すべてのデータが正常であることを意味し、外側のループを終了できます。
高度な最適化
public static void bubble_v2(int[] a) {
int n = a.length - 1;
while (true) {
int last = 0; // 表示最后一次交换索引位置
for (int i = 0; i < n; i++) {
System.out.println("比较次数" + i);
if (a[i] > a[i + 1]) {
Utils.swap(a, i, i + 1);
last = i;
}
}
n = last;
System.out.println("第轮冒泡"
+ Arrays.toString(a));
if (n == 0) {
break;
}
}
}
- バブリングの各ラウンド中に、最後の交換インデックスをバブリングの次のラウンドの比較回数として使用できます。この値が 0 の場合は、配列全体が正常であることを意味し、外側のループを終了するだけで済みます。
3. 選択ソート
必要とする
- 選択ソートアルゴリズムを自分の言語で説明できる
- 選択ソートとバブルソートを比較する機能
- 不安定なソートと安定したソートを理解する
アルゴリズムの説明
- 配列をソート済みとソートされていない 2 つのサブセットに分割します。各ラウンドで、ソートされていないサブセットから最小の要素が選択され、ソートされたサブセットに入れられます。
- 配列全体がソートされるまで、上記の手順を繰り返します。
より具体的な説明については、selection_sort.html を参照してください。
アルゴリズムの実装
public static void selection(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
// i 代表每轮选择最小元素要交换到的目标索引
int s = i; // 代表最小元素的索引
for (int j = s + 1; j < a.length; j++) {
if (a[s] > a[j]) { // j 元素比 s 元素还要小, 更新 s
s = j;
}
}
if (s != i) {
swap(a, s, i);
}
System.out.println(Arrays.toString(a));
}
}
- 最適化ポイント: 交換の数を減らすために、まず各ラウンドで最小のインデックスを見つけ、各ラウンドの終わりに要素を交換します。
バブルソートと比較する
- 両方の平均時間計算量は次のとおりです。
- 選択ソートは交換が少ないため、一般にバブリングより高速です。
- ただし、コレクションが高度に順序付けされている場合は、選択よりもバブリングの方が優れています。
- バブルは安定した並べ替えアルゴリズムですが、選択は不安定な並べ替えアルゴリズムです
-
- 安定した並べ替えとは、同じ値を持つ要素の順序を乱すことなく、オブジェクト内の異なるフィールドによる複数の並べ替えを指します。
- 不安定なソートの場合はその逆が当てはまります
安定ソートと不安定ソート
System.out.println("=================不稳定================");
Card[] cards = getStaticCards();
System.out.println(Arrays.toString(cards));
selection(cards, Comparator.comparingInt((Card a) -> a.sharpOrder).reversed());
System.out.println(Arrays.toString(cards));
selection(cards, Comparator.comparingInt((Card a) -> a.numberOrder).reversed());
System.out.println(Arrays.toString(cards));
System.out.println("=================稳定=================");
cards = getStaticCards();
System.out.println(Arrays.toString(cards));
bubble(cards, Comparator.comparingInt((Card a) -> a.sharpOrder).reversed());
System.out.println(Arrays.toString(cards));
bubble(cards, Comparator.comparingInt((Card a) -> a.numberOrder).reversed());
System.out.println(Arrays.toString(cards));
それらはすべて、最初にスーツ (♠♥♣♦) で並べ替えられ、次に番号 (AKQJ…) で並べ替えられます。
- 不安定な並べ替えアルゴリズムが数値で並べ替えると、同じ値を持つスーツの順序が崩れます。
[[♠7], [♠2], [♠4], [♠5], [♥2], [♥5]]
[[♠7], [♠5], [♥5], [♠4], [♥2], [♠2]]
♠2が前、♥2が後ろで、数字に従って並べ替えられ、位置が変わっていることがわかります。
- 安定した並べ替えアルゴリズムが数値で並べ替える場合、以下に示すように、同じ値の元のスーツの順序が保持されます。 ♠2 と ♥2 の相対的な位置は変更されません。
[[♠7], [♠2], [♠4], [♠5], [♥2], [♥5]]
[[♠7], [♠5], [♥5], [♠4], [♠2], [♥2]]
4. 挿入ソート
必要とする
- 挿入ソートアルゴリズムを自分の言語で説明できる
- 挿入ソートと選択ソートを比較する機能
アルゴリズムの説明
- 配列をソート領域と未ソート領域の 2 つの領域に分割し、各ラウンドで未ソート領域から最初の要素を取得し、ソート領域に挿入します (順序を保証する必要があります)。
- 配列全体がソートされるまで、上記の手順を繰り返します。
より具体的な説明については、insert_sort.html を参照してください。
アルゴリズムの実装
// 修改了代码与希尔排序一致
public static void insert(int[] a) {
// i 代表待插入元素的索引
for (int i = 1; i < a.length; i++) {
int t = a[i]; // 代表待插入的元素值
int j = i;
System.out.println(j);
while (j >= 1) {
if (t < a[j - 1]) { // j-1 是上一个元素索引,如果 > t,后移
a[j] = a[j - 1];
j--;
} else { // 如果 j-1 已经 <= t, 则 j 就是插入位置
break;
}
}
a[j] = t;
System.out.println(Arrays.toString(a) + " " + j);
}
}
選択ソートと比較する
- 両方の平均時間計算量は次のとおりです。
- ほとんどの場合、選択よりも挿入の方がわずかに優れています
- ソートセット挿入の時間計算量は次のとおりです。
- 挿入は安定した並べ替えアルゴリズムですが、選択は不安定な並べ替えアルゴリズムです
ヒント
挿入ソートは通常、学生によって過小評価されていますが、そのステータスは非常に重要です。データ量が少ない並べ替えの場合は、挿入並べ替えが優先されます。
5. ヒルソート
必要とする
- ヒルソートアルゴリズムを自分の言語で説明できる
アルゴリズムの説明
- まず、(n/2、n/4 ... 1) などのギャップ シーケンスを選択します。n は配列の長さです。
- 各ラウンドでは、等しいギャップを持つ要素をグループとしてみなし、そのグループ内の要素を 2 つの目的で挿入およびソートします (①
少数の要素の挿入とソートの速度が非常に速い
)。グループ内の値が大きいほど、後ろに早く移動します - 差が徐々に減り、1になるとソート完了
より具体的な説明については、shell_sort.html を参照してください。
アルゴリズムの実装
private static void shell(int[] a) {
int n = a.length;
for (int gap = n / 2; gap > 0; gap /= 2) {
// i 代表待插入元素的索引
for (int i = gap; i < n; i++) {
int t = a[i]; // 代表待插入的元素值
int j = i;
while (j >= gap) {
// 每次与上一个间隙为 gap 的元素进行插入排序
if (t < a[j - gap]) { // j-gap 是上一个元素索引,如果 > t,后移
a[j] = a[j - gap];
j -= gap;
} else { // 如果 j-1 已经 <= t, 则 j 就是插入位置
break;
}
}
a[j] = t;
System.out.println(Arrays.toString(a) + " gap:" + gap);
}
}
}
参考文献
6. クイックソート
必要とする
- クイックソートのアルゴリズムを自分の言語で説明できること
- 手書きの片側ループコードと両側ループコードのいずれかをマスターする
- クイックソートの特徴を説明できる
- Lomuto 分割方式とホール分割方式のパフォーマンスの比較を理解する
アルゴリズムの説明
- ソートの各ラウンドで、パーティショニングの参照点 (ピボット) が選択されます。
-
- 基準点より小さい要素を 1 つのパーティションに入力し、基準点より大きい要素を別のパーティションに入力します。
- パーティショニングが完了すると、ピボット要素の位置が最終位置になります。
- サブパーティション要素の数が 1以下になるまで、サブパーティション内で上記のプロセスを繰り返します。これは、分割統治の考え方を反映しています。
- 上記の説明からわかるように、鍵は分割アルゴリズムにあり、一般的なものには、ロムート分割方式、双方向ループ分割方式、およびホール分割方式が含まれます。
より具体的な説明については、quick_sort.html を参照してください。
片側巡回高速ソート (lomuto 分割スキーム)
- データム要素として右端の要素を選択します
- j ポインタは参照点よりも小さい要素を見つける役割を果たし、要素が見つかると i と交換されます。
- i ポインタは、参照点より小さい要素の境界を維持し、各スワップのターゲット インデックスでもあります。
- 最後に、基準点が i と交換され、i は分割位置になります。
public static void quick(int[] a, int l, int h) {
if (l >= h) {
return;
}
int p = partition(a, l, h); // p 索引值
quick(a, l, p - 1); // 左边分区的范围确定
quick(a, p + 1, h); // 左边分区的范围确定
}
private static int partition(int[] a, int l, int h) {
int pv = a[h]; // 基准点元素
int i = l;
for (int j = l; j < h; j++) {
if (a[j] < pv) {
if (i != j) {
swap(a, i, j);
}
i++;
}
}
if (i != h) {
swap(a, h, i);
}
System.out.println(Arrays.toString(a) + " i=" + i);
// 返回值代表了基准点元素所在的正确索引,用它确定下一轮分区的边界
return i;
}
双方向循環高速ソーティング (ホーア ホール パーティション スキームと厳密には同等ではありません)
- 左端の要素をデータム要素として選択します
- j ポインタは参照点より小さい要素を右から左に見つける役割を果たし、i ポインタは参照点より大きい要素を左から右に見つける役割を果たします。見つかったら、この 2 つは i、j が交差するまで交換されます。
- 最後に基準点をiと交換します(このときiとjは等しい)、iは分割位置になります
主なポイント
- 基準点は左側にあり、j の後に i が続く必要があります。
- while( i < j && a[j] > pv ) j–
- while ( i < j && a[i] <= pv ) i++
private static void quick(int[] a, int l, int h) {
if (l >= h) {
return;
}
int p = partition(a, l, h);
quick(a, l, p - 1);
quick(a, p + 1, h);
}
private static int partition(int[] a, int l, int h) {
int pv = a[l];
int i = l;
int j = h;
while (i < j) {
// j 从右找小的
while (i < j && a[j] > pv) {
j--;
}
// i 从左找大的
while (i < j && a[i] <= pv) {
i++;
}
swap(a, i, j);
}
swap(a, l, j);
System.out.println(Arrays.toString(a) + " j=" + j);
return j;
}
クイックソート機能
- 平均時間計算量は、最悪の時間計算量はです
- データ量が多い場合、その利点は非常に明白です
- 不安定な種類です
ロムートゾーニングスキームとホールゾーニングスキーム
- ホールの移動量は平均してロムートの 3 分の 1 です
- https://qastack.cn/cs/11458/quicksort-partitioning-hoare-vs-lomuto
補足コードの説明
- day01.sort.QuickSort3 は、キャビテーションによって改善された比較を減らした両面クイックソートを示します。
- day01.sort.QuickSortHoare は、Hoare パーティショニングの実装を示します。
- Day01.sort.LomutoVsHoare は、4 つのパーティションによって達成された移動数を比較します。
7. 配列リスト
必要とする
- マスター ArrayList 展開ルール
展開ルール
- ArrayList() は長さ 0 の配列を使用します
- ArrayList(int initialCapacity) は、指定された容量の配列を使用します
- public ArrayList(Collection<? extends E> c) は c のサイズを配列の容量として使用します
- add(Object o) は初めて容量を 10 に拡張し、再度容量を前の容量の 1.5 倍に拡張します
- addAll(Collection c)は、要素がない場合はMath.max(10、実際の要素数)、要素がある場合はMath.max(元の容量の1.5倍、実際の要素数)に展開されます。
このうち、4 番目の点は必ず知っておく必要があり、その他の点は個別の状況によって異なります。
ヒント
- テストコードを参照してください
day01.list.TestArrayList
。ここにはリストされません。 - なお、この例ではArrayListの展開特性をより直感的に反映するためにリフレクションを使用していますが、JDK 9以降はモジュール化の影響でリフレクションの制限が増え、実行時にVMパラメータを追加する必要があります。実行するテストコードは
--add-opens java.base/java.util=ALL-UNNAMED
成功しました。次の例はすべて同じ問題を抱えています
コードの説明
- day01.list.TestArrayList#arrayListGrowRule は、add(Object) メソッドの展開ルールを示し、入力パラメータ n は展開された配列の長さを出力する回数を表します。
8. イテレータ
必要とする
- フェイルファストとは何か、フェイルセーフとは何かをマスターする
フェイルファストとフェイルセーフ
- ArrayList はフェイルファストの代表的なもので、トラバース中に変更することはできず、できるだけ早く失敗します。
- CopyOnWriteArrayList はフェールセーフの代表的なもので、トラバース中に変更でき、原則は読み取りと書き込みの分離です。
ヒント
- テストコードを参照してください
day01.list.FailFastVsFailSafe
。ここにはリストされません。
9. リンクリスト
必要とする
- LinkedList と ArrayList の違いを明確に説明でき、いくつかの間違いを修正することに注意を払うことができる
リンクリスト
- 二重リンクリストに基づいており、連続メモリは必要ありません
- ランダムアクセスが遅い(リンクリストに沿った走査)
- 高性能による頭から末尾までの挿入と削除
- 多くのメモリを消費する
配列リスト
- 配列ベース、連続したメモリが必要
- 高速ランダムアクセス(添え字によるアクセスを指します)
- 末尾の挿入と削除のパフォーマンスは問題ありませんが、挿入と削除の他の部分ではデータが移動されるため、パフォーマンスが低下します。
- CPUキャッシュ、局所性原理を使用可能
コードの説明
- day01.list.ArrayListVsLinkedList#randomAccess とランダム アクセスのパフォーマンス
- day01.list.ArrayListVsLinkedList#addMiddle は中央に挿入した場合のパフォーマンスを比較します
- day01.list.ArrayListVsLinkedList#addFirst ヘッダー挿入パフォーマンスの比較
- day01.list.ArrayListVsLinkedList#addLast と末尾挿入のパフォーマンス
- day01.list.ArrayListVsLinkedList#linkedListSize メモリを占有している LinkedList を出力します
- day01.list.ArrayListVsLinkedList#arrayListSize メモリを占有している ArrayList を出力します
10. ハッシュマップ
必要とする
- HashMapの基本的なデータ構造をマスターする
- マスターツリー化
- インデックスの計算方法、セカンダリ ハッシュの意味、インデックス計算に対する容量の影響を理解する
- プットプロセス、拡張、拡張係数をマスターする
- HashMap の同時使用によって発生する可能性のある問題を理解する
- キーの設計を理解する
1) 基本的なデータ構造
- 1.7 配列 + リンクリスト
- 1.8 配列+ (リンクリスト | 赤黒ツリー)
より鮮明なデモについては、データ内の hash-demo.jar を参照してください。操作には jdk14 以降の環境が必要です。jar パッケージ ディレクトリに入り、次のコマンドを実行します。
java -jar --add-exports java.base/jdk.internal.misc=ALL-UNNAMED hash-demo.jar
2) 樹木化と分解
木の意味
- 赤黒ツリーは、DoS 攻撃を回避し、リンク リストが長すぎる場合のパフォーマンスの低下を防ぐために使用されます。ツリーは偶発的な状況である必要があり、最終的な戦略です。
- ハッシュ テーブルの検索と更新の時間計算量は、赤黒ツリーの検索と更新の時間計算量は、TreeNode は通常の Node よりも多くのスペースを占有します。必要がない場合は、リンク リストを使用してみてください。
- ハッシュ値が十分にランダムである場合、ハッシュ テーブル内のポアソンに従って分散されます。負荷係数 0.75 の場合、長さが 8 を超えるリンク リストの確率は 0.00000006 です。ツリー化しきい値は次のように選択されます。ツリー化確率を十分に小さくするには 8 にする必要があります
ツリールール
- リンク リストの長さがツリー化のしきい値 8 を超える場合、まずリンク リストの長さを減らすために容量の拡張を試みます。アレイの容量が 64 以上の場合、ツリー化が実行されます。
退化したルール
- ケース 1: 容量拡張時にツリーを分割する際、ツリー要素数が 6 以下の場合、リンク リストは縮退します
- ケース 2: ツリー ノードを削除するときに、root、root.left、root.right、root.left.left のいずれかが null の場合、リンク リストに縮退します。
3) 指数計算
指数の算出方法
- まず、オブジェクトの hashCode() を計算します。
- 次に、二次ハッシュのために HashMap の hash() メソッドを呼び出します。
-
- 2 番目の hash() は、高レベルのデータを合成し、ハッシュ分布をより均一にすることです。
- 最後に & (容量 – 1) でインデックスを取得します
アレイ容量が 2 の n 乗になるのはなぜですか
- インデックスを計算するときはより効率的です。インデックスが 2 の n 乗である場合は、モジュロの代わりにビットごとの AND 演算を使用できます。
- 展開時にインデックスを再計算する方が効率的です。ハッシュ & oldCap == 0 の要素は元の位置に残ります。それ以外の場合は、新しい位置 = 古い位置 + oldCap になります。
知らせ
- 2 番目のハッシュは、容量が 2 の n 乗であるという設計前提と一致するためです。ハッシュ テーブルの容量が 2 の n 乗でない場合、2 番目のハッシュは必要ありません。
- 容量は 2 の n 乗です。この設計はインデックスの計算効率が高くなりますが、ハッシュの分散が悪く、補償として 2 番目のハッシュが必要です。この設計を採用しない典型的な例は Hashtable です。
4) 配置と拡張
プットプロセス
- HashMap は配列を遅延的に作成し、配列は初めて使用されるときにのみ作成されます。
- インデックスの計算 (バケットの添字)
- バケットの添字がまだ占有されていない場合は、ノードのプレースホルダーを作成して戻ります。
- バケットの添字がすでに占有されている場合
-
- TreeNode ブラック ツリーの追加または更新ロジックはすでに人気があります
- 通常のノードであり、リンク リストの追加または更新ロジックに従います。リンク リストの長さがツリーのしきい値を超える場合は、ツリー ロジックに従います。
- 返却前に容量が閾値を超えていないか確認し、超えた場合は容量を拡張する
1.7と1.8の違い
- リンクリストにノードを挿入する場合、1.7 は先頭挿入方法、1.8 は末尾挿入方法です。
- 1.7 はしきい値以上で容量を拡張するスペースがありません。1.8 はしきい値を超えて容量を拡張できません。
- 1.8 ノードインデックスを展開して計算すると最適化されます
拡張(ロード)係数がデフォルトで 0.75f に設定されているのはなぜですか
- スペース使用量とクエリ時間の間の適切なトレードオフ
- この値より大きい場合、スペースは節約されますが、リンクされたリストが長くなり、パフォーマンスに影響します。
- この値より小さい場合、競合は減少しますが、容量拡張がより頻繁になり、スペースがより多く占有されます。
5) 同時実行の問題
拡張デッドチェーン (1.7 に存在する予定)
1.7 ソースコードは次のとおりです。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
- e と next は両方とも、現在のノードと次のノードを指すために使用されるローカル変数です。
- スレッド 1 (緑色) の一時変数 e と next はこれら 2 つのノードを参照したばかりで、今後ノードを移動できるようになる前にスレッドの切り替えが発生し、スレッド 2 (青色) が拡張と移行を完了します。
- スレッド 2 の拡張が完了しましたが、ヘッドの挿入方法により、リンク リストの順序が逆になります。ただし、スレッド 1 の一時変数 e と next は依然としてこれら 2 つのノードを参照しているため、別の移行が必要です。
- 最初のサイクル
-
- ループはスレッドが切り替わる前に実行されます。この時点では、e はノード a を指し、次はノード b を指すことに注意してください。
- e の先頭にノードを挿入します。図ではノードのコピーが 2 つありますが、実際には 1 つだけであることに注意してください (矢印が落書きされるのを防ぐために 2 つのコピーが描画されています)。
- ループが終了すると、e は次の b ノードを指します。
- 2番目のサイクル
-
- 次はノード a を指します
- e ヘッダー挿入ポイント b
- ループが終了すると、e は次のノード a を指します。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-A3enIgab-1691920287269)()]
- 3番目のサイクル
-
- next は null を指します
- e ヘッダーはノード a を挿入し、a の次は b を指します(a.next が常に null になる前)、b の次は a を指し、デッドリンクは
- ループが終了すると、e は null である next を指すため、4 番目のループで正常に終了します。
データ障害 (1.7、1.8 は存在します)
- コードリファレンス
day01.map.HashMapMissData
、具体的なデバッグ手順についてはビデオを参照してください。
補足コードの説明
- day01.map.HashMapDistribution は、マップ内のリンク リストの長さがポアソン分布に準拠していることを示します。
- day01.map.DistributionAffectedByCapacity は、容量と hashCode 値が分布に及ぼす影響を示します。
-
- day01.map.DistributionAffectedByCapacity#hashtableGrowRule は Hashtable の展開ルールを示します
- day01.sort.Utils#randomArray ハッシュコードが十分にランダムである場合、容量が 2 の n 乗であるかどうかはほとんど影響しません。
- day01.sort.Utils#lowSameArray ハッシュコードの下位ビット数が同じ場合、容量は 2 の n 乗となり、不均一な分布になります。
- day01.sort.Utils#evenArray 偶数のハッシュコードが多く、容量が2のn乗の場合、分布が不均一になります
- このことから、2 番目のハッシュは、容量が 2 の n 乗である設計にとって非常に重要であると結論付けることができます。
- day01.map.HashMapVsHashtable は、同じ数の単語列に対する HashMap と Hashtable の分布の違いを示しています。
6) キーデザイン
主要な設計要件
- HashMap のキーは null にできますが、Map の他の実装では null にできません。
- キーオブジェクトとして、hashCodeとequalsを実装する必要があり、キーの内容は変更できません(不変)
- キーの hashCode は良好なハッシュ可能性を持っている必要があります
キーが可変の場合、たとえば年齢が変更された場合、再度クエリを実行してもクエリは実行されません。
public class HashMapMutableKey {
public static void main(String[] args) {
HashMap<Student, Object> map = new HashMap<>();
Student stu = new Student("张三", 18);
map.put(stu, new Object());
System.out.println(map.get(stu));
stu.age = 19;
System.out.println(map.get(stu));
}
static class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
}
String オブジェクトの hashCode() 設計
- 目標は、比較的均一なハッシュ効果を実現することであり、各文字列のハッシュコードは十分に一意です。
- 文字列内の各文字は と呼ばれる数値として表現できます。i の範囲は 0 ~ n - 1 です。
- ハッシュの式は次のとおりです。
- 31 置換式はハッシュ特性が優れており、31 * h は次のように最適化できます。
-
- つまり、$32 ∗h -h $
- 今すぐ
- 今すぐ
11. シングルトンパターン
必要とする
- 5 つのシングルトン パターンの実装をマスターする
- DCL が静的変数を変更するために volatile を実装する理由を理解する
- JDK でシングルトンが使用されるシナリオを理解する
お腹を空かせた中華風
public class Singleton1 implements Serializable {
private Singleton1() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("private Singleton1()");
}
private static final Singleton1 INSTANCE = new Singleton1();
public static Singleton1 getInstance() {
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
public Object readResolve() {
return INSTANCE;
}
}
- コンストラクターは、リフレクションによるシングルトンの破壊を防ぐために例外をスローします。
readResolve()
逆シリアル化によってシングルトンが破壊されるのを防ぐためです
中国風を列挙する
public enum Singleton2 {
INSTANCE;
private Singleton2() {
System.out.println("private Singleton2()");
}
@Override
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public static Singleton2 getInstance() {
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
- 列挙により、リフレクションと逆シリアル化によるシングルトンの破壊を自然に防ぐことができます。
怠け者
public class Singleton3 implements Serializable {
private Singleton3() {
System.out.println("private Singleton3()");
}
private static Singleton3 INSTANCE = null;
// Singleton3.class
public static synchronized Singleton3 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
- 実際、同期が必要になるのはシングルトン オブジェクトが最初に作成されたときのみですが、コードは実際には呼び出されるたびに同期されます。
- そこで、次のダブルチェックロックの改善により、
ダブルチェックロックの怠惰なスタイル
public class Singleton4 implements Serializable {
private Singleton4() {
System.out.println("private Singleton4()");
}
private static volatile Singleton4 INSTANCE = null; // 可见性,有序性
public static Singleton4 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton4.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton4();
}
}
}
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
volatile を追加する必要がある理由:
INSTANCE = new Singleton4()
アトミックではなく、3 つのステップに分かれています: オブジェクトの作成、コンストラクターの呼び出し、静的変数への値の割り当て。最後の 2 つのステップは命令の並べ替えによって最適化される可能性があり、最初に割り当てになり、次にコンストラクターを呼び出します。- スレッド 1 が最初に代入を実行し、スレッド 2 が
INSTANCE == null
最初の代入に到達したときに INSTANCE が null ではないことが判明した場合、不完全に構築されたオブジェクトを返します。
インナークラスの怠け者
public class Singleton5 implements Serializable {
private Singleton5() {
System.out.println("private Singleton5()");
}
private static class Holder {
static Singleton5 INSTANCE = new Singleton5();
}
public static Singleton5 getInstance() {
return Holder.INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
- ダブルチェックロックのデメリットを回避する
JDKにおけるシングルトンの具体化
- ランタイムは飢えた中国人のシングルトンを体現する
- コンソールには二重チェックされたロックの遅延シングルトンが組み込まれています
- コレクション内の EmptyNavigableSet 内部クラスの遅延シングルトン
- ReverseComparator.REVERSE_ORDER 内部クラスの遅延シングルトン
- Comparators.NaturalOrderComparator.INSTANCE は中国式のシングルトンを列挙します
同時記事
1. スレッドの状態
必要とする
- Java スレッドの 6 つの状態をマスターする
- Java スレッドの状態遷移をマスターする
- 5つの州と6つの州の違いを理解できる
6 つの状態と遷移
それぞれ
- 新しいビルド
-
- スレッド オブジェクトが作成されたが、start メソッドが呼び出されなかった場合、スレッド オブジェクトは新しい状態になります。
- 現時点ではオペレーティング システムの基礎となるスレッドに関連付けられていません
- 実行可能
-
- start メソッドを呼び出した後、新しく作成されたメソッドから実行可能ファイルに入ります。
- 現時点では、基礎となるスレッドに関連付けられており、オペレーティング システムによる実行がスケジュールされています。
- 終わり
-
- スレッド内のコードが実行され、実行可能からファイナライズに入ります。
- このとき、基となるスレッドとの関連付けは解除されます。
- ブロック
-
- ロックの取得が失敗すると、モニターに実行できるブロッキング キューがブロックされ、現時点では CPU 時間は占有されません。
- ロック保持スレッドがロックを解放すると、特定のルールに従ってブロッキング キュー内のブロックされたスレッドがウェイクアップされ、ウェイクアップされたスレッドは実行可能状態になります。
- 待って
-
- ロックの取得に成功したものの、条件が満たされていない場合は、 wait() メソッドが呼び出されます。このとき、ロックは実行可能状態から解放され、 wait に設定されたモニター待機に入ります。 CPU時間。
- 他のロック保持スレッドがnotify()またはnotifyAll()メソッドを呼び出すと、待機セット内の待機スレッドが特定のルールに従ってウェイクアップされ、実行可能な状態に復元されます。
- 制限時間待ち
-
- ロックの取得に成功したものの、条件を満たしていない場合は wait(long) メソッドが呼び出され、このときロックは実行可能状態から解放され、時限待ちの監視待ち状態に移行します。 CPU時間を占有しません。
- 他のロック保持スレッドがnotify()またはnotifyAll()メソッドを呼び出すと、待機セット内の時間制限付き待機スレッドが特定のルールに従ってウェイクアップされ、実行可能な状態に復元され、ロックをめぐって再競合されます。
- 待機がタイムアウトになった場合も、時間制限付き待機状態から実行可能な状態に回復し、ロックを再競合します。
- もう 1 つの状況は、sleep(long) メソッドを呼び出すと、実行可能状態から時間制限付きの待機状態に入りますが、これはモニターとは何の関係もなく、積極的にウェイクアップする必要はありません。自然に実行可能な状態に戻ります
その他の状況 (知っておく必要があるだけ)
- Interrupt() メソッドを使用すると、時間制限のある待機中のスレッドを中断し、実行可能な状態に戻すことができます。
- パーク、パーク解除、その他のメソッドでもスレッドを待機させたり、ウェイクアップさせたりすることができます
5つの州
5 つの州の声明は、オペレーティング システム レベルの分割に由来しています。
- 実行状態: CPU 時間に割り当てられ、実際にスレッドでコードを実行できます。
- 準備完了状態: CPU 時間の対象ですが、まだ順番が来ていません
- ブロック状態: CPU 時間の対象外
-
- Java 状態で言及されているブロッキング、待機、時間指定待機をカバーします。
- より多くのブロッキング I/O が存在します。つまり、スレッドがブロッキング I/O を呼び出すと、実際の作業は I/O デバイスによって完了します。この時点では、スレッドは何もすることがなく、待機することしかできません。
- 新しい最終状態: Java の同じ名前の状態に似ていますが、冗長ではありません。
2. スレッドプール
必要とする
- スレッド プールの 7 つのコア パラメータをマスターする
7 つのパラメータ
- corePoolSize コアスレッドの数 - プール内に保持されるスレッドの最大数
- MaximumPoolSize スレッドの最大数 - コア スレッドの最大数 + レスキュー スレッド
- keepAliveTime 生存時間 - レスキュー スレッドの生存時間。生存時間内に新しいタスクがない場合、このスレッド リソースは解放されます。
- 単位時間単位 - 緊急スレッドの生存時間単位 (秒、ミリ秒など)。
- workQueue - アイドル状態のコア スレッドがない場合、新しいタスクはこのキューに入れられ、キューがいっぱいになると、タスクを実行するための緊急スレッドが作成されます。
- threadFactory スレッド ファクトリ - スレッド名の設定、デーモン スレッドかどうかなど、スレッド オブジェクトの作成をカスタマイズできます。
- ハンドラー拒否戦略 - すべてのスレッドがビジーで、workQueue がいっぱいの場合、拒否戦略がトリガーされます。
-
- 例外 java.util.concurrent.ThreadPoolExecutor.AbortPolicy をスローする
- タスクは呼び出し元 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy によって実行されます。
- タスクの破棄 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
- キューに入れられた最も古いタスクを破棄する java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-U9e6Mevj-1691920287271)()]
コードの説明
day02.TestThreadPoolExecutor は、スレッド プールのコア構成をより鮮明な方法で示します。
3. 待機 vs 睡眠
必要とする
- 違いが分かるようになる
1 つの共通点、3 つの相違点
共通点
- wait()、wait(long)、sleep(long) の効果は、現在のスレッドが一時的に CPU を使用する権利を放棄し、ブロッキング状態になることです。
違い
- メソッドの帰属が異なります
-
- sleep(long) は Thread の静的メソッドです
- wait()、wait(long) はすべて Object のメンバー メソッドであり、各オブジェクトには
- 違う時間に起きる
-
- sleep(long) と wait(long) を実行するスレッドは、対応するミリ秒間待機した後にウェイクアップします。
- wait(long) と wait() は、notify によって起動することもできます。wait() が起動しない場合は、永久に待機します。
- 彼らはすべて目を覚ますために中断することができます
- さまざまなロック特性 (強調)
-
- wait メソッドの呼び出しでは、まず wait オブジェクトのロックを取得する必要がありますが、sleep にはそのような制限はありません
- wait メソッドが実行されると、オブジェクト ロックが解放され、他のスレッドがオブジェクト ロックを取得できるようになります (CPU は放棄しますが、まだ使用できます)。
- また、同期されたコード ブロックでスリープが実行された場合、オブジェクト ロックは解放されません (CPU を放棄するため、使用できなくなります)。
4. ロックと同期
必要とする
- ロックと同期の違いをマスターする
- ReentrantLock の公平なロックと不公平なロックを理解する
- ReentrantLock の条件変数を理解する
3つのレベル
違い
- 文法レベル
-
- synchronized はキーワードであり、ソース コードは jvm であり、C++ 言語で実装されています。
- ロックはインターフェースであり、ソースコードはjdkによって提供され、java言語で実装されます。
- synchronizedを使用する場合、終了同期コードブロックのロックは自動的に解放されますが、Lockを使用する場合は、手動でunlockメソッドを呼び出してロックを解放する必要があります。
- 機能レベル
-
- どちらもペシミスティック ロックに属し、基本的な相互排他、同期、およびロックの再エントリ機能を備えています。
- Lock は、待機状態の取得、Fair Lock、割り込み可能、タイムアウト、複数の条件変数など、Synchronized にはない多くの機能を提供します。
- Lock には、ReentrantLock、ReentrantReadWriteLock など、さまざまなシナリオに適した実装があります。
- パフォーマンスレベル
-
- 競合がない場合、同期は偏ったロックや軽量ロックなどの多くの最適化を実行しており、パフォーマンスは悪くありません。
- 一般に、ロックの実装は、競合が多い場合にパフォーマンスが向上します。
フェアロック
- フェアロックのフェアな具現化
-
- ブロッキング キュー内に既に存在するスレッド(タイムアウトに関係なく) は、常に先入れ先出しで公平です。
- 公平なロックとは、ブロッキング キューにないスレッドがロックをめぐって競合することを指します。キューが空でない場合は、キューの最後まで正直に待ちます。
- 不公平なロックとは、ブロッキング キューにないスレッドがロックをめぐって競合し、キュー ヘッドによって起動されたスレッドと競合することを意味します。
- 公平なロックはスループットを低下させるため、通常は使用されません
条件変数
- ReentrantLock の条件変数の機能は、通常の同期待機と通知に似ています。これは、スレッドがロックを取得し、条件が満たされていないことが判明したときに、リンク リスト構造で一時待機するために使用されます。
- 同期待機セットとの違いは、ReentrantLock では複数の条件変数を使用できることで、より詳細な待機とウェイクアップの制御を実現できます。
コードの説明
- day02.TestReentrantLock は、ReentrantLock の内部構造をより鮮明に示します。
5.揮発性
必要とする
- スレッドセーフを習得する際に考慮すべき 3 つの問題
- volatileを使いこなすことでどのような問題が解決できるのか
原子性
- 原因: マルチスレッドでは、異なるスレッドの命令がインターリーブされるため、共有変数の読み取りと書き込みの混乱が生じます。
- 解決策: ペシミスティック ロックまたはオプティミスティック ロックを使用します。volatile ではアトミック性を解決できません。
可視性
- 原因:他のスレッドには表示されない、コンパイラの最適化、キャッシュの最適化、または CPU 命令の並べ替えの最適化による共有変数の変更
- 解決策: 共有変数を volatile で修飾すると、コンパイラなどの最適化が行われなくなり、あるスレッドによる共有変数の変更が別のスレッドに見えるようになります。
秩序
- 原因:コンパイラの最適化、キャッシュの最適化、または CPU 命令の並べ替えの最適化により、命令の実際の実行順序が書き込み順序と一致しません。
- 解決策: volatile を使用してシェア変数を変更すると、シェア変数の読み取りおよび書き込み時に異なるバリアが追加され、他の読み取りおよび書き込み操作がバリアを越えることができなくなり、並べ替えを防止する効果が得られます。
- 知らせ:
-
- volatile 変数によって追加されるバリアは、バリアを超える他の書き込み操作がvolatile 変数の下でキューに入れられるのを防ぐためのものです。
- volatile 変数の読み取りの障壁は、以下の他の読み取り操作が障壁を越えてvolatile変数の読み取りより上位にランクされることを防ぐことです。
- 揮発性の読み取りおよび書き込みによって追加されるバリアは、同じスレッド内での命令の並べ替えのみを防止できます。
コードの説明
- day02.threadsafe.AddAndSubtract はアトミック性を示します
- day02.threadsafe.ForeverLoop デモの可視性
-
- 注: この例は、コンパイラの最適化によって引き起こされる可視性の問題であることが証明されています。
- day02.threadsafe.Reordering は順序付けを示します
-
- jar パッケージにパッケージ化してテストする必要がある
- 動画解説も参考にしてください
6. 悲観的ロックと楽観的ロック
必要とする
- 悲観的ロックと楽観的ロックの違いをマスターする
悲観的ロックと楽観的ロックの比較
- 悲観的なロックの代表的なものは同期され、ロックがロックされます。
-
- 中心的な考え方は、[スレッドは、ロックを所有している場合にのみ共有変数を操作できます。毎回 1 つのスレッドだけがロックを正常に占有することができ、ロックの取得に失敗したスレッドは停止して待機する必要があります] です。
- スレッドの実行からブロック、そしてブロックからウェイクアップまでの間にスレッド コンテキストの切り替えが発生し、これが頻繁に発生するとパフォーマンスに影響します。
- 実際、スレッドが同期ロックを取得し、ロックがすでに占有されている場合は、ブロックされる可能性を減らすために数回再試行します。
- 楽観的ロックの代表的なものは AtomicInteger で、これは cas を使用して原子性を保証します。
-
- その中心的なアイデアは、[ロックする必要はなく、毎回 1 つのスレッドだけが共有変数を正常に変更でき、失敗した他のスレッドは停止する必要がなく、成功するまで再試行し続ける] です。
- スレッドは常に実行されているため、ブロックする必要がなく、スレッド コンテキストの切り替えは必要ありません。
- マルチコア CPU のサポートが必要であり、スレッド数が CPU コアの数を超えてはなりません
コードの説明
- day02.SyncVsCas は、アトミックな割り当てを解決するための楽観的ロックと悲観的ロックの使用を示します。
- 動画解説も参考にしてください
7. ハッシュテーブルと同時ハッシュマップ
必要とする
- Hashtable と ConcurrentHashMap の違いをマスターする
- さまざまなバージョンにおける ConcurrentHashMap の実装の違いをマスターする
より鮮明なデモについては、データ内の hash-demo.jar を参照してください。操作には jdk14 以降の環境が必要です。jar パッケージ ディレクトリに入り、次のコマンドを実行します。
java -jar --add-exports java.base/jdk.internal.misc=ALL-UNNAMED hash-demo.jar
ハッシュテーブルと同時ハッシュマップ
- Hashtable と ConcurrentHashMap はどちらもスレッドセーフな Map コレクションです
- ハッシュテーブルの同時実行性は低く、ハッシュテーブル全体がロックに対応し、同時に操作できるスレッドは 1 つだけです
- ConcurrentHashMap は同時実行性が高く、ConcurrentHashMap 全体が複数のロックに対応しているため、スレッドが異なるロックにアクセスする限り競合は発生しません。
同時ハッシュマップ 1.7
- データ構造:
Segment(大数组) + HashEntry(小数组) + 链表
、各セグメントはロックに対応し、複数のスレッドが異なるセグメントにアクセスしても競合は発生しません。 - 同時実行性: セグメント配列のサイズが同時実行性であり、同時にアクセスできるスレッドの数が決まります。Segment 配列は拡張できません。つまり、同時実行性は ConcurrentHashMap の作成時に固定されます。
- 指数計算
-
- 大きな配列の長さが であると仮定すると、大きな配列内のキーのインデックスは、キーのセカンダリ ハッシュ値の上位 m ビットです。
- 小さな配列の長さが であると仮定すると、小さな配列内のキーのインデックスは、キーの 2 次ハッシュ値の下位 n ビットです。
- 拡張: 各小さな配列の拡張は比較的独立しており、小さな配列が拡張係数を超えると拡張がトリガーされ、毎回拡張が 2 倍になります。
- Segment[0] プロトタイプ: 初めて他の小さな配列を作成する場合、このプロトタイプがベースとして使用され、配列の長さと拡張率はプロトタイプに基づきます。
同時ハッシュマップ 1.8
- データ構造:
Node 数组 + 链表或红黑树
、配列の各ヘッド ノードはロックとして使用され、複数のスレッドによってアクセスされるヘッド ノードが異なる場合、競合は発生しません。初めてヘッド ノードを生成するときに競合が発生した場合は、パフォーマンスをさらに向上させるために、同期の代わりに cas を使用します。 - 同時実行: Node 配列のサイズは同時実行と同じですが、1.7 とは異なり、Node 配列を拡張できます。
- 拡張条件:ノード配列が3/4いっぱいになると容量が拡張されます
- 拡張ユニット: リンク リストを単位として使用し、リンク リストを後ろから前に移行します。移行が完了したら、古いアレイ ヘッド ノードを ForwardingNode に置き換えます。
- 拡張時の同時取得
-
- 新しい配列で検索するか古い配列で検索するかを ForwardingNode が決定するかどうかに応じて、ブロックしません
- リンクされたリストの長さが 1 を超える場合は、ノードの移行後に次のポインターが変更されることを恐れて、ノードをコピーする (新しいノードを作成する) 必要があります。
- リンクされたリストの最後のいくつかの要素のインデックスが展開後も変更されない場合、ノードをコピーする必要はありません。
- 容量拡張中の同時プット
-
- put スレッドが拡張スレッド操作と同じリンク リストである場合、put スレッドはブロックされます。
- put スレッド操作のリンク リストが移行されていない場合、つまりヘッド ノードが ForwardingNode でない場合は、同時に実行できます。
- put スレッド操作のリンク リストが移行されている場合、つまりヘッド ノードが ForwardingNode である場合、拡張を支援できます。
- 1.7 と比較した遅延初期化
- 容量は要素の推定数を表し、容量 / ファクトリは初期配列サイズの計算に使用されます。これは次の値に近い必要があります。
- loadFactor は、初期配列サイズを計算する場合にのみ使用され、その後の拡張は 3/4 に固定されます。
- ツリーのしきい値を超えたときの拡張の問題。容量がすでに 64 の場合は直接ツリーを実行し、それ以外の場合は元の容量に基づいて 3 ラウンドの拡張を実行します。
8. スレッドローカル
必要とする
- ThreadLocalの機能と原理をマスターする
- ThreadLocalのメモリ解放タイミングを把握する
効果
- ThreadLocal は、[リソース オブジェクト] のスレッド分離を実現し、各スレッドが独自の [リソース オブジェクト] を使用できるようにし、競合によって引き起こされるスレッドの安全性の問題を回避できます。
- ThreadLocal はスレッド内でのリソース共有も実装します
原理
各スレッドには ThreadLocalMap タイプのメンバー変数があり、リソース オブジェクトを格納するために使用されます。
- set メソッドを呼び出すと、ThreadLocal 自体がキーとして、リソース オブジェクトが値として使用され、それが現在のスレッドの ThreadLocalMap コレクションに追加されます。
- get メソッドを呼び出すと、ThreadLocal 自体がキーとして使用され、現在のスレッドで関連するリソース値が検索されます。
- Remove メソッドを呼び出すと、ThreadLocal 自体をキーとして使用して、現在のスレッドに関連付けられたリソース値を削除します。
ThreadLocalMap のいくつかの機能
- キーのハッシュ値は均一に分散されます
- 初期容量は16、拡張倍率は2/3、拡張容量は2倍になります
- キーインデックスの競合後にオープンアドレス指定を使用して競合を解決する
弱い参照キー
ThreadLocalMap のキーは、次の理由から弱い参照として設計されています。
- スレッドは長時間実行する必要がある場合があります (スレッド プール内のスレッドなど)。キーが使用されなくなった場合は、メモリが不足したときにキーが占有しているメモリを解放する必要があります (GC)。
メモリ解放時間
- パッシブ GC がキーを解放する
-
- キーのメモリのみが解放され、値に関連付けられたメモリは解放されません。
- 価値の怠惰な受動的リリース
-
- キーを取得するときに、それが null キーであることが判明した場合は、その値のメモリを解放します。
- キーを設定するとき、ヒューリスティック スキャンを使用して、隣接する null キーの値メモリをクリアします。ヒューリスティックの数は、要素の数と null キーが見つかったかどうかに関係します。
- アクティブに削除してキーと値を解放します
-
- キーと値のメモリは同時に解放され、隣接する null キーの値のメモリもクリアされます
- これを使用することをお勧めします。これは、一般に ThreadLocal を使用するときに静的変数 (つまり、強参照) として使用されるため、GC リサイクルに受動的に依存することができないためです。