Адрес видео: 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, другие элементы слева от среднего значения меньше Т, сравнивать не надо, найти средний индекс справа, поставить М+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
это может привести к выходу за пределы диапазона целых чисел и вызвать ошибки в работе. Есть два решения:
int m = l + (r - l) / 2;
Другой:
int m = (l + r) >>> 1;
Другие методы
- Существует упорядоченный список 1, 5, 8, 11, 19, 22, 31, 35, 40, 45, 48, 49, 50. Когда найден узел с двоичным значением поиска 48, количество сравнений, необходимое для успешный поиск
- При использовании метода дихотомии для нахождения элемента 81 в последовательности 1,4,6,7,15,33,39,50,64,78,75,81,89,96 необходимо пройти () сравнения
- Для двоичного поиска числа в массиве из 128 элементов максимальное количество сравнений требуется не более, чем сколько раз
Для первых двух вопросов запомните краткую формулу судейства: два нечетных очка занимают середину, а четные два очка – середину слева. Для последнего вопроса нужно знать формулу:
Где n — количество поисков, а N — количество элементов
2. Пузырьковая сортировка
Требовать
- Уметь описать алгоритм пузырьковой сортировки на своем родном языке.
- Возможность написать код пузырьковой сортировки вручную
- Понимать некоторые методы оптимизации пузырьковой сортировки.
Алгоритм Описание
- Сравните размеры двух соседних элементов в массиве по очереди. Если a[j] > a[j+1], то поменяйте местами два элемента. Сравнение обоих из них называется раундом всплытия. Результатом является расположение наибольшего элемент, чтобы наконец
- Повторяйте вышеуказанные шаги, пока весь массив не будет отсортирован.
Более наглядное описание см. на странице 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: после каждого раунда барботирования внутренний цикл можно сократить один раз.
- Точка оптимизации 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;
}
}
}
- Во время каждого раунда барботирования последний обменный индекс может использоваться как количество сравнений для следующего раунда барботирования.Если это значение равно нулю, это означает, что весь массив в порядке, и вы можете просто выйти из внешнего цикла
3. Сортировка выбором
Требовать
- Уметь описать алгоритм сортировки выбором на своем родном языке
- Возможность сравнения сортировки выбором с пузырьковой сортировкой
- Понимание нестабильной сортировки и стабильной сортировки
Алгоритм Описание
- Разделите массив на два подмножества, отсортированное и несортированное, каждый раунд выбирает наименьший элемент из несортированного подмножества и помещает его в отсортированное подмножество.
- Повторяйте вышеуказанные шаги, пока весь массив не будет отсортирован.
Более наглядное описание см. на странице 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. Сортировка вставками
Требовать
- Уметь описать алгоритм сортировки вставками на своем родном языке
- Возможность сравнения сортировки вставками с сортировкой выбором
Алгоритм Описание
- Разделите массив на две области, отсортированную область и несортированную область, каждый раунд берет первый элемент из несортированной области и вставляет его в отсортированную область (порядок должен быть гарантирован)
- Повторяйте вышеуказанные шаги, пока весь массив не будет отсортирован.
Более наглядное описание см. на странице insertion_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 — длина массива
- В каждом раунде элементы с равными промежутками рассматриваются как группа, а элементы в группе вставляются и сортируются для двух целей ① Скорость вставки и сортировки
небольшого количества элементов очень высока
② Пусть элементы с большие значения в группе перемещаются назад быстрее - Когда разрыв постепенно уменьшается до 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 и Hall
Алгоритм Описание
- Каждый раунд сортировки выбирает опорную точку (стержень) для разделения.
-
- Пусть элементы меньше точки отсчета входят в один раздел, а элементы больше точки отсчета входят в другой раздел
- Когда разбиение завершено, положение поворотного элемента является его конечным положением.
- Повторяйте описанный выше процесс в подразделе до тех пор, пока количество элементов подраздела не станет меньше или равно 1, что отражает идею «разделяй и властвуй».
- Как видно из приведенного выше описания, ключ лежит в алгоритме разбиения, общие включают схему разбиения Ломуто, схему разбиения двустороннего цикла и схему разбиения Холла.
Более наглядное описание см. на странице quick_sort.html.
Односторонняя циклическая быстрая сортировка (схема ломуто-разбиения)
- Выберите самый правый элемент в качестве базового элемента
- Указатель 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 отвечает за поиск элементов, больших, чем контрольная точка, слева направо После того, как они найдены, они обмениваются до тех пор, пока i, j не пересекутся.
- Наконец, контрольная точка заменяется на i (в это время i и j равны), и i является позицией раздела
Основные моменты
- Контрольная точка находится слева, а за j должно следовать i
- в то время как ( i < j && a[j] > pv ) j–
- в то время как ( я < j && a [i] <= pv ) я ++
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 раза меньше, чем Ломуто
- https://qastack.cn/cs/11458/quicksort-partitioning-hoare-vs-lomuto
Дополнительный код Описание
- day01.sort.QuickSort3 демонстрирует двухстороннюю быструю сортировку с улучшенными свойствами кавитации с меньшим количеством сравнений
- day01.sort.QuickSortHoare демонстрирует реализацию разделения Хоара
- Day01.sort.LomutoVsHoare сравнивает количество ходов, выполненных четырьмя разделами.
7. Список массивов
Требовать
- Основные правила расширения ArrayList
Правила расширения
- ArrayList() будет использовать массив нулевой длины
- 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 раза больше исходной емкости, фактическое количество элементов)
Среди них необходимо знать четвертый пункт, а остальные пункты зависят от индивидуальных обстоятельств.
намекать
- См. тестовый код
day01.list.TestArrayList
, его здесь не будет - Следует отметить , что в примере используется отражение для более интуитивного отражения характеристик расширения ArrayList, но начиная с JDK 9 из-за влияния модульности на отражение больше ограничений, и необходимо добавлять параметры ВМ при запуске тестовый код для
--add-opens java.base/java.util=ALL-UNNAMED
запуска Пройдено, все следующие примеры имеют одну и ту же проблему
описание кода
- day01.list.TestArrayList#arrayListGrowRule демонстрирует правила расширения метода add(Object), а входной параметр n представляет, сколько раз печатать длину расширенного массива.
8. Итератор
Требовать
- Узнайте, что такое Fail-Fast и Fail-Safe
Отказоустойчивость и отказоустойчивость
- ArrayList — типичный представитель отказоустойчивости, он не может быть изменен при обходе и дает сбой при первой возможности.
- CopyOnWriteArrayList — типичный представитель отказоустойчивости, его можно модифицировать при обходе, принцип — разделение чтения-записи.
намекать
- См. тестовый код
day01.list.FailFastVsFailSafe
, его здесь не будет
9. Связанный список
Требовать
- Уметь четко объяснить разницу между LinkedList и ArrayList и обратить внимание на исправление некоторых ошибок
Связанный список
- На основе двусвязного списка не требуется непрерывная память
- Произвольный доступ медленный (обход по связанному списку)
- Вставка и удаление «голова к хвосту» с высокой производительностью
- Занимает много памяти
ArrayList
- На основе массива, требует непрерывной памяти
- Быстрый произвольный доступ (имеется в виду доступ по индексу)
- Производительность вставки и удаления хвоста в порядке, но другие части вставки и удаления будут перемещать данные, поэтому производительность будет низкой.
- Может использовать кеш процессора, принцип локальности
описание кода
- 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 равен нулю, он выродится в связанный список.
3) Расчет индекса
Метод расчета индекса
- Сначала вычислите hashCode() объекта
- Затем вызовите метод hash() HashMap для вторичного хеширования.
-
- Второй метод hash() предназначен для синтеза высокоуровневых данных и более равномерного распределения хэшей.
- Наконец & (capacity – 1) получает индекс
Почему емкость массива равна энной степени 2
- Это более эффективно при вычислении индекса: если это n-я степень числа 2, вы можете использовать побитовую операцию И вместо по модулю.
- При расширении индекс эффективнее пересчитывать: элемент с хэшем и oldCap == 0 остается в исходной позиции, иначе новая позиция = старая позиция + oldCap
Уведомление
- Второй хеш должен соответствовать предпосылке проекта, согласно которой емкость равна n-й степени числа 2. Если емкость хэш-таблицы не является n-й степенью числа 2, второй хеш не нужен.
- Емкость равна n-й степени числа 2. Этот дизайн более эффективен при вычислении индексов, но дисперсия хэша не очень хороша, и в качестве компенсации необходим второй хэш. Типичным примером, который не использует этот дизайн, является Hashtable.
4) поставить и расширить
положить процесс
- HashMap создает массив лениво, и массив создается только при первом использовании
- Вычислить индекс (индекс корзины)
- Если индекс ведра еще не занят, создайте заполнитель Node и верните
- Если индекс ведра уже занят
-
- Уже популярная логика добавления или обновления черного дерева 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 являются локальными переменными, используемыми для указания на текущий узел и следующий узел.
- Временные переменные e и next потока 1 (зеленый) только что ссылались на эти два узла, и прежде чем узел можно будет переместить в будущем, происходит переключение потока, и поток 2 (синий) завершает расширение и миграцию.
- Завершено расширение потока 2. Из-за метода вставки заголовка порядок связанного списка обратный. Однако временные переменные e и next потока 1 по-прежнему относятся к этим двум узлам, поэтому требуется еще одна миграция.
- первый цикл
-
- Цикл выполняется до переключения потока, обратите внимание, что в это время e указывает на узел a, а затем указывает на узел b.
- Вставьте узел в голову е. Обратите внимание, что на картинке две копии узла, а на самом деле только одна (две копии нарисованы для того, чтобы стрелку не нацарапать)
- Когда цикл закончится, e укажет на следующий, который является узлом b
- второй цикл
-
- следующий указывает на узел a
- e точка вставки заголовка b
- Когда цикл заканчивается, e указывает на следующий, который является узлом a
[Не удалось передать изображение по внешней ссылке, на исходном сайте может быть механизм защиты от пиявки, рекомендуется сохранить изображение и загрузить его напрямую (img-A3enIgab-1691920287269)()]
- третий цикл
-
- следующий указывает на ноль
- Заголовок e вставляет узел a, следующий из a указывает на b (ранее a.next всегда был нулевым), следующий из b указывает на a, и неработающая ссылка стала
- Когда цикл заканчивается, e указывает на следующий, который равен нулю, поэтому он нормально выйдет в четвертом цикле.
Неупорядоченность данных (1.7, 1.8 будут существовать)
- Справочник по коду
day01.map.HashMapMissData
, конкретные шаги по отладке см. в видео
Дополнительный код Описание
- day01.map.HashMapDistribution демонстрирует, что длина связанного списка на карте соответствует распределению Пуассона.
- day01.map.DistributionAffectedByCapacity демонстрирует влияние емкости и значения hashCode на распределение
-
- day01.map.DistributionAffectedByCapacity#hashtableGrowRule демонстрирует правило расширения Hashtable
- day01.sort.Utils#randomArray Если hashCode достаточно случайный, значение емкости равное 2 в степени n не имеет большого значения.
- day01.sort.Utils#lowSameArray Если hashCode имеет одинаковое количество младших бит, емкость равна 2 в n-й степени, что приведет к неравномерному распределению
- day01.sort.Utils#evenArray Если имеется много четных чисел hashCode и емкость равна 2 в энной степени, распределение будет неравномерным.
- Из этого можно сделать вывод, что второй хэш очень важен для конструкции, емкость которой равна n-й степени числа 2.
- day01.map.HashMapVsHashtable демонстрирует разницу в распределении HashMap и Hashtable для одного и того же количества строк слов.
6) Ключевой дизайн
основные требования к дизайну
- Ключ HashMap может быть нулевым, но в других реализациях Map это не так.
- В качестве ключевого объекта должны быть реализованы hashCode и equals, а содержимое ключа не может быть изменено (неизменяемое).
- Хэш-код ключа должен иметь хорошую хэшируемость.
Если ключ является переменным, например, если возраст изменен, он не будет запрашиваться при повторном запросе.
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);
}
}
}
hashCode() дизайн объекта String
- Цель состоит в том, чтобы добиться относительно однородного хеш-эффекта, а хеш-код каждой строки достаточно уникален.
- Каждый символ в строке может быть выражен как число, называемое , где диапазон i равен 0 ~ n - 1.
- Формула хеша:
- Формула подстановки 31 имеет лучшие хэш-свойства, а 31*h можно оптимизировать как
-
- То есть $32 ∗h -h $
- Прямо сейчас
- Прямо сейчас
11. Одноэлементный паттерн
Требовать
- Освойте реализацию пяти одноэлементных паттернов
- Понять, почему 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 шага: создать объект, вызвать конструктор и присвоить значение статической переменной.Последние два шага могут быть оптимизированы путем переупорядочения инструкций и стать сначала присваиванием, а затем вызовом конструктора- Если поток 1 выполняет присваивание первым, а поток 2
INSTANCE == null
обнаруживает, что INSTANCE не равен нулю, когда он достигает первого, он вернет не полностью построенный объект.
ленивый внутренний класс
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
- Освоение переходов между состояниями потока Java
- Может понять разницу между пятью состояниями и шестью состояниями
Шесть состояний и переходов
соответственно
- новая сборка
-
- Когда объект потока создан, но метод запуска не был вызван, он находится в новом состоянии .
- В настоящее время не связан с базовым потоком операционной системы.
- работоспособный
-
- После вызова метода start он войдет в runnable из только что созданного
- В настоящее время он связан с базовым потоком и запланирован для выполнения операционной системой.
- конец
-
- Код в потоке был выполнен и входит в финализацию из runnable
- В это время связь с базовым потоком будет отменена.
- блокировать
-
- Когда получение блокировки не удается, блокируется очередь блокировки , которая может запускаться в монитор , и в это время процессорное время не занято.
- Когда удерживающий блокировку поток освобождает блокировку, он пробуждает заблокированный поток в очереди блокировки в соответствии с определенными правилами, и пробужденный поток переходит в состояние выполнения.
- ждать
-
- Когда блокировка получена успешно, но поскольку условие не выполнено, вызывается метод wait().В это время блокировка освобождается от рабочего состояния и входит в состояние ожидания монитора, установленное на ожидание , которое также не занимает процессор. время.
- Когда другие потоки, удерживающие блокировку, вызывают метод notify() или notifyAll(), ожидающие потоки в ожидающем наборе будут разбужены в соответствии с определенными правилами и восстановлены в работоспособном состоянии .
- срок ожидания
-
- Когда блокировка получена успешно, но поскольку условие не выполнено, вызывается метод ожидания (длительный).В это время блокировка освобождается от рабочего состояния и переходит в режим ожидания монитора для ограниченного по времени ожидания , который также не занимает процессорное время.
- Когда другие потоки, удерживающие блокировку, вызывают метод notify() или notifyAll(), ожидающие потоки с ограниченным временем ожидания в наборе ожидающих будут пробуждены в соответствии с определенными правилами , восстановлены в работоспособном состоянии и повторно конкурируют за блокировку.
- Если время ожидания истекло, он также восстановится из состояния ожидания с ограничением по времени в состояние готовности к выполнению и повторно соревнуется за блокировку.
- Другая ситуация заключается в том, что вызов метода sleep(long) также перейдет в состояние ожидания с ограничением по времени из состояния runnable , но это не имеет ничего общего с монитором и не требует активного пробуждения. он естественным образом вернется в работоспособное состояние
Другие ситуации (просто нужно знать)
- Вы можете использовать метод interrupt() для прерывания ожидающих , ограниченных по времени ожидающих потоков и восстановления их в работоспособное состояние .
- Park, unpark и другие методы также могут заставить потоки ждать и просыпаться.
пять штатов
Заявление о пяти состояниях исходит из разделения на уровне операционной системы.
- Состояние выполнения: выделяется процессорному времени, может выполнять код в потоке.
- Состояние готовности: подходит для процессорного времени, но еще не настала очередь
- Заблокированное состояние: не подходит для процессорного времени
-
- Охватывает блокировку , ожидание , ожидание по времени, упомянутое в состоянии Java.
- Существует больше блокирующего ввода-вывода, что означает, что когда поток вызывает блокирующий ввод-вывод, фактическая работа завершается устройством ввода-вывода.В это время потоку нечего делать и он может только ждать.
- Новое и конечное состояние: аналогично состоянию с тем же именем в java, больше не многословно
2. Пул потоков
Требовать
- Освойте 7 основных параметров пула потоков
Семь параметров
- corePoolSize количество основных потоков — максимальное количество потоков, которые будут храниться в пуле
- maxPoolSize максимальное количество потоков - максимальное количество основных потоков + спасательных потоков
- keepAliveTime Survival time - время выживания спасательного потока, если в течение времени выживания нет новых задач, этот ресурс потока будет освобожден
- unit time unit - единица времени выживания аварийного потока, например секунды, миллисекунды и т.д.
- workQueue — когда нет свободных основных потоков, новые задачи будут поставлены в очередь в этой очереди, а когда очередь будет заполнена, будут созданы аварийные потоки для выполнения задач.
- Фабрика потоков threadFactory — вы можете настроить создание объектов потока, например, установить имя потока, является ли он потоком демона и т. д.
- стратегия отклонения обработчика — когда все потоки заняты и рабочая очередь заполнена, будет запущена стратегия отклонения
-
- Выдать исключение 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. ждать или спать
Требовать
- уметь различать
Одна общность, три отличия
точки соприкосновения
- Эффекты wait(), wait(long) и sleep(long) заключаются в том, что текущий поток временно отказывается от права использовать ЦП и переходит в состояние блокировки.
разница
- Метод атрибуции отличается
-
- sleep(long) — это статический метод Thread
- И wait(), wait(long) — все методы-члены Object, каждый объект имеет
- Просыпайтесь в разное время
-
- Потоки, выполняющие sleep(long) и wait(long), проснутся после ожидания соответствующих миллисекунд.
- wait(long) и wait() также могут быть разбужены уведомлением, если wait() не проснется, оно будет ждать вечно
- Их всех можно прервать, чтобы проснуться
- Различные характеристики замка (акцент)
-
- Вызов метода ожидания должен сначала получить блокировку объекта ожидания, в то время как сон не имеет такого ограничения.
- После выполнения метода ожидания блокировка объекта будет снята, что позволит другим потокам получить блокировку объекта (я отказываюсь от процессора, но вы все равно можете его использовать)
- И если сон выполняется в синхронизированном блоке кода, он не снимет объектную блокировку (я отказываюсь от процессора, и вы не можете его использовать)
4. блокировка против синхронизации
Требовать
- Освойте разницу между блокировкой и синхронизацией
- Понимание честных и нечестных блокировок ReentrantLock
- Понимание условных переменных в ReentrantLock
три уровня
разница
- грамматический уровень
-
- synchronized — это ключевое слово, исходный код в jvm, реализованный на языке c++
- Lock — это интерфейс, исходный код предоставлен jdk и реализован на языке java.
- При использовании синхронизированного блокировка блока кода выхода синхронизации будет снята автоматически, но при использовании блокировки вам необходимо вручную вызвать метод разблокировки, чтобы снять блокировку
- функциональный уровень
-
- Обе относятся к пессимистическим блокировкам, и обе имеют базовые функции взаимного исключения, синхронизации и повторного входа в блокировку.
- Блокировка предоставляет множество функций, которых нет у synchronized, например, получение состояния ожидания, справедливая блокировка, прерывание, тайм-аут и несколько переменных условий.
- Блокировка имеет реализации, подходящие для разных сценариев, такие как ReentrantLock, ReentrantReadWriteLock.
- уровень исполнения
-
- Когда нет конкуренции, синхронизированный сделал много оптимизаций, таких как предвзятые блокировки, облегченные блокировки, и производительность неплохая.
- Реализации блокировки обычно обеспечивают лучшую производительность при высокой конкуренции.
честный замок
- Честное воплощение честного замка
-
- Потоки , уже находящиеся в очереди на блокировку (независимо от тайм-аута), всегда выполняются по принципу «первым пришел — первым вышел».
- Справедливая блокировка относится к потокам , которые не находятся в очереди блокировки, чтобы конкурировать за блокировку.Если очередь не пуста, честно дождаться конца очереди
- Несправедливая блокировка означает, что потоки , не находящиеся в очереди блокировки, конкурируют за блокировку и конкурируют с потоком, пробужденным головой очереди.
- Честные блокировки снижают пропускную способность и обычно не используются.
переменная условия
- Функция переменной условия в ReentrantLock аналогична обычному синхронизированному ожиданию и уведомлению, которое используется в структуре связанного списка для временного ожидания, когда поток получает блокировку и обнаруживает, что условие не выполняется.
- Отличие от синхронизированного набора ожидания заключается в том, что в ReentrantLock может быть несколько условных переменных, что позволяет добиться более точного управления ожиданием и пробуждением.
описание кода
- day02.TestReentrantLock более наглядно демонстрирует внутреннюю структуру ReentrantLock
5. летучий
Требовать
- Три вопроса, которые следует учитывать при освоении безопасности потоков
- Какие проблемы можно решить, освоив volatile
атомарность
- Причина: при многопоточности инструкции разных потоков чередуются, что приводит к путанице при чтении и записи общих переменных.
- Решение: используйте пессимистичные блокировки или оптимистичные блокировки, volatile не может решить проблему атомарности.
видимость
- Причина: изменения общих переменных из-за оптимизации компилятора, оптимизации кэша или оптимизации переупорядочения инструкций ЦП, которые не видны другим потокам.
- Решение: украшение общих переменных с помощью volatile может предотвратить выполнение оптимизаций, таких как компиляторы, и сделать изменение общих переменных одним потоком видимым для другого потока.
упорядоченность
- Причина: из-за оптимизации компилятора, оптимизации кэша или оптимизации переупорядочения инструкций ЦП фактический порядок выполнения инструкций не соответствует порядку записи.
- Решение: изменение общих переменных с помощью volatile добавит различные барьеры при чтении и записи общих переменных, предотвращая пересечение барьеров другими операциями чтения и записи, тем самым достигая эффекта предотвращения переупорядочения.
- Уведомление:
-
- Барьер , добавляемый переменной volatile , предназначен для предотвращения постановки в очередь других операций записи над барьером под переменной volatile.
- Барьер для чтения энергозависимой переменной заключается в том, чтобы другие операции чтения ниже не пересекали барьер и не ранжировались выше чтения энергозависимой переменной.
- Барьеры, добавляемые непостоянными операциями чтения и записи, могут предотвратить изменение порядка инструкций только в одном потоке.
описание кода
- day02.threadsafe.AddAndSubtract демонстрирует атомарность
- Day02.threadsafe.ForeverLoop видимость демонстрации
-
- Примечание. Было доказано, что этот пример является проблемой видимости, вызванной оптимизацией компилятора.
- day02.threadsafe.Reordering демонстрирует порядок
-
- Его нужно упаковать в баночку и протестировать
- Пожалуйста, также обратитесь к пояснению к видео
6. Пессимистическая блокировка против оптимистичной блокировки
Требовать
- Освойте разницу между пессимистичной блокировкой и оптимистичной блокировкой.
Сравнение пессимистической блокировки и оптимистичной блокировки
- Представителями пессимистичных замков являются синхронизированные и замковые замки.
-
- Основная идея заключается в следующем: [Потоки могут оперировать общими переменными только в том случае, если они владеют блокировкой. Только один поток может успешно занимать блокировку каждый раз, и поток, который не может получить блокировку, должен остановиться и ждать]
- Поток от запуска до блокировки, а затем от блокировки до пробуждения включает в себя переключение контекста потока. Если это происходит часто, это влияет на производительность.
- На самом деле, когда поток получает синхронизацию и блокировку блокировки, если блокировка уже занята, он делает несколько повторных попыток, чтобы уменьшить вероятность блокировки.
- Представителем оптимистичной блокировки является AtomicInteger, который использует cas для обеспечения атомарности.
-
- Его основная идея заключается в том, что [нет необходимости в блокировке, только один поток может каждый раз успешно изменять общую переменную, другие потоки, потерпевшие неудачу, не должны останавливаться, продолжайте повторять попытки до тех пор, пока они не будут успешными]
- Поскольку поток всегда выполняется, нет необходимости блокировать его, поэтому переключение контекста потока не требуется.
- Требуется поддержка многоядерных процессоров, а количество потоков не должно превышать количество ядер процессора.
описание кода
- day02.SyncVsCas демонстрирует использование оптимистичных и пессимистичных блокировок для решения атомарных задач.
- Пожалуйста, также обратитесь к пояснению к видео
7. Hashtable против ConcurrentHashMap
Требовать
- Освойте разницу между Hashtable и ConcurrentHashMap
- Изучите различия в реализации ConcurrentHashMap в разных версиях.
Для более наглядной демонстрации см. hash-demo.jar в данных.Для операции требуется среда jdk14 или выше.Войдите в каталог пакета jar и выполните следующую команду
java -jar --add-exports java.base/jdk.internal.misc=ALL-UNNAMED hash-demo.jar
Hashtable против ConcurrentHashMap
- И Hashtable, и ConcurrentHashMap являются потокобезопасными коллекциями карт.
- Hashtable имеет низкий уровень параллелизма, вся Hashtable соответствует блокировке, и одновременно с ней может работать только один поток.
- ConcurrentHashMap имеет высокий уровень параллелизма, и вся ConcurrentHashMap соответствует нескольким блокировкам.Пока потоки обращаются к разным блокировкам, конфликтов не будет.
ConcurrentHashMap 1.7
- Структура данных:
Segment(大数组) + HashEntry(小数组) + 链表
каждый сегмент соответствует блокировке, если несколько потоков обращаются к разным сегментам, конфликта не будет. - Параллелизм: размер массива Segment — это параллелизм, который определяет, сколько потоков могут получить одновременный доступ. Массив Segment не может быть расширен, что означает, что параллелизм фиксируется при создании ConcurrentHashMap.
- расчет индекса
-
- Предполагая, что длина большого массива равна , индекс ключа в большом массиве равен старшим m битам вторичного хеш-значения ключа.
- Предполагая, что длина маленького массива равна , индекс ключа в маленьком массиве равен младшим n битам вторичного хеш-значения ключа.
- Расширение: расширение каждого небольшого массива относительно независимое.Когда небольшой массив превышает коэффициент расширения, расширение будет запущено, и расширение будет удваиваться каждый раз.
- Прототип Segment[0]: При создании других небольших массивов в первый раз, этот прототип будет использоваться в качестве основы, длина массива и коэффициент расширения будут основаны на прототипе.
ConcurrentHashMap 1.8
- Структура данных:
Node 数组 + 链表或红黑树
каждый головной узел массива используется как замок, если головные узлы, к которым обращаются несколько потоков, различны, конфликта не будет. Если конкуренция возникает при создании головного узла в первый раз, используйте cas вместо synchronized для дальнейшего повышения производительности. - Параллелизм: размер массива узлов такой же, как и параллелизм.В отличие от 1.7, массив узлов можно расширить.
- Условие расширения: когда массив узлов заполнен на 3/4, емкость будет расширена.
- Блок расширения: используйте связанный список как единицу для переноса связанного списка с заднего на передний.После завершения переноса замените старый головной узел массива на ForwardingNode.
- Параллельное получение во время расширения
-
- В зависимости от того, решает ли ForwardingNode поиск в новом массиве или в старом массиве, он не будет блокироваться.
- Если длина связанного списка превышает 1, нужно скопировать узел (создать новый узел), опасаясь, что следующий указатель изменится после переноса узла
- Если после расширения индекс нескольких последних элементов связанного списка остается неизменным, узел копировать не нужно.
- Параллельная установка во время расширения емкости
-
- Если поток размещения является тем же связанным списком, что и операция потока расширения, поток размещения будет заблокирован.
- Если связанный список операции запуска потока не был перенесен, то есть головной узел не является ForwardingNode, он может выполняться одновременно
- Если связанный список операции помещенного потока был перенесен, то есть головным узлом является ForwardingNode, он может помочь в расширении.
- Ленивая инициализация по сравнению с 1.7
- емкость представляет предполагаемое количество элементов, а емкость/фабрика используется для расчета начального размера массива, который должен быть близок к
- loadFactor используется только при расчете начального размера массива, а затем расширение фиксируется на 3/4
- Проблема расширения при превышении порога дерева, если емкость уже 64, сразу дерево, иначе делать 3 раунда расширения на основе исходной емкости
8. Локальный поток
Требовать
- Освойте функцию и принцип ThreadLocal
- Узнайте о времени освобождения памяти ThreadLocal
эффект
- ThreadLocal может реализовать изоляцию потоков [объектов ресурсов], позволить каждому потоку использовать свои собственные [объекты ресурсов] и избежать проблем с безопасностью потоков, вызванных конфликтами.
- ThreadLocal также реализует совместное использование ресурсов в потоках.
принцип
Каждый поток имеет переменную-член типа ThreadLocalMap, которая используется для хранения объектов ресурсов.
- Вызов метода set заключается в использовании самого ThreadLocal в качестве ключа и объекта ресурса в качестве значения и помещении его в коллекцию ThreadLocalMap текущего потока.
- Вызов метода get заключается в использовании самого ThreadLocal в качестве ключа для поиска связанного значения ресурса в текущем потоке.
- Вызов метода удаления заключается в использовании самого ThreadLocal в качестве ключа для удаления значения ресурса, связанного с текущим потоком.
Некоторые возможности ThreadLocalMap
- Хэш-значение ключа равномерно распределено
- Начальная емкость равна 16, коэффициент расширения равен 2/3, а емкость расширения удваивается.
- Используйте открытую адресацию для разрешения конфликтов после конфликтов ключевых индексов.
слабый ссылочный ключ
Ключ в ThreadLocalMap разработан как слабая ссылка по следующим причинам.
- Поток может выполняться в течение длительного времени (например, потоки в пуле потоков), если ключ больше не используется, занимаемая им память должна быть освобождена при нехватке памяти (GC).
время освобождения памяти
- Пассивный сборщик мусора выпускает ключи
-
- Освобождается только память ключа, а память, связанная со значением, освобождаться не будет
- Ленивое пассивное высвобождение ценности
-
- При получении ключа, если он оказался нулевым, освободить его память значений
- При установке ключа будет использоваться эвристическое сканирование для очистки памяти значений соседнего нулевого ключа.Количество эвристик связано с количеством элементов и найден ли нулевой ключ
- Активно удалить, чтобы освободить ключ, значение
-
- Память ключа и значения будет освобождена одновременно, и память значения соседнего нулевого ключа также будет очищена.
- Его рекомендуется использовать, потому что он обычно используется как статическая переменная (то есть сильная ссылка) при использовании ThreadLocal, поэтому он не может пассивно полагаться на перезапуск GC.