Что я знаю о серии JDK, часть 5 и принципе ThreadLocal?
1. Вступительные замечания
Сегодня я хочу резюмировать ThreadLocal, Основные темы:
- Введение в ThreadLocal
- Принцип реализации ThreadLocal, включая базовую структуру данных, метод хэширования, коллизию хэшей, механизм расширения емкости и т. д.
- Анализ утечки памяти ThreadLocal
- Сценарии и примеры приложений ThreadLocal
Впервые я услышал о ThreadLocal, когда еще практиковался в 2018. В то время была задача, которая использовала пулы потоков, некоторые люди говорили, что проблема параллелизма также может быть решена с помощью ThreadLocal. Но я не использовал эту штуку в то время, оставив лишь смутное впечатление, что "ее можно использовать для решения проблем параллелизма".
До сих пор я также буду использовать ThreadLocal в проекте, но если я хочу подробно объяснить принцип его реализации, я чувствую, что он все еще немного расплывчатый, поэтому я воспользовался свободным временем, чтобы посмотреть исходный код ThreadLocal. и выскажу свое мнение.Пойми и поделись со всеми всеми фантазиями.
2. Введение в ThreadLocal
Как говорится в примечаниях JDK: «Класс ThreadLocal предоставляет локальные переменные потока, которые обычно являются статическими полями в частных классах, которые хотят связать состояние с потоками».
Короче говоря, ThreadLocal предоставляетИзоляция данных между потокамифункции, вы также можете узнать из ее имени, что это локальная переменная, принадлежащая потоку. Другими словами, каждый поток будет сохранять фрагмент данных, уникальный для потока, в ThreadLocal, поэтому он является потокобезопасным.
Учащиеся, знакомые со Spring, могут знать область действия Bean, а областью действия ThreadLocal является поток.
Мы можем продемонстрировать возможности ThreadLocal на простом примере:
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 创建一个有2个核心线程数的线程池
ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10));
// 线程池提交一个任务,将任务序号及执行该任务的子线程的线程名放到 ThreadLocal 中
threadPool.execute(() -> threadLocal.set("任务1: " + Thread.currentThread().getName()));
threadPool.execute(() -> threadLocal.set("任务2: " + Thread.currentThread().getName()));
threadPool.execute(() -> threadLocal.set("任务3: " + Thread.currentThread().getName()));
// 输出 ThreadLocal 中的内容
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> System.out.println("ThreadLocal value of " + Thread.currentThread().getName() + " = " + threadLocal.get()));
}
// 线程池记得关闭
threadPool.shutdown();
}
Вышеприведенный код сначала создает общий пул потоков с двумя основными потоками, затем отправляет задачу, помещает номер задачи и имя потока дочернего потока, выполняющего задачу, в ThreadLocal и, наконец, выводит пул потоков в цикле for Сохраненное значение в ThreadLocal для каждого потока. Вывод этой программы:
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任务3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任务2: pool-1-thread-2
Видно, что задача в пуле потоков, которая выполняет отправленную задачу, называетсяpool-1-thread-1
Thread, а затем несколько раз вывести содержимое, хранящееся в переменной ThreadLocal основного потока пула потоков, также показывает, что содержимое, хранящееся в ThreadLocal каждого потока, уникально для текущего потока.В многопоточной среде это может эффективно предотвратить собственные переменные, измененные другими потоками (кроме случаев, когда хранимое содержимое является объектом того же ссылочного типа).
2. Принцип реализации ThreadLocal
В версии JDK1.8 исходный код класса ThreadLocal имеет в общей сложности 723 строки, а без комментариев около 350. Его следует рассматривать как класс с относительно небольшим объемом кода в основной библиотеке классов JDK. Условно говоря, его исходный код довольно прост для понимания.
Теперь поговорим о принципе его реализации из структуры данных ThreadLocal.
2.1 Базовая структура данных
Сначала откройте заголовок главы:Нижний уровень ThreadLocal хранит данные через статический внутренний класс ThreadLocalMap. ThreadLocalMap — это карта пар ключ-значение. Его нижний уровень — это массив объектов Entry. Ключ, хранящийся в объекте Entry, — это объект ThreadLocal, а значение конкретное содержимое хранилища типа объекта..
Кроме того, ThreadLocalMap также является свойством класса Thread.
Как доказать правильность базовой структуры данных класса ThreadLocal, приведенной выше? мы можем начать сThreadLocal#get()
Метод начинает отслеживать код, чтобы увидеть, откуда берутся локальные переменные потока.
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
ThreadLocalMap map = getMap(t);
// 若 threadLocals 变量不为空,根据 ThreadLocal 对象来获取 key 对应的 value
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 若 threadLocals 变量是 NULL,初始化一个新的 ThreadLocalMap 对象
return setInitialValue();
}
// ThreadLocal#setInitialValue
// 初始化一个新的 ThreadLocalMap 对象
private T setInitialValue() {
// 初始化一个 NULL 值
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// ThreadLocalMap#createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
пройти черезThreadLocal#get()
В методе хорошо видно, что при чтении данных из ThreadLocal по объекту ThreadLocal мы сначала получим объект текущего потока, а затем получим свойство threadLocals типа ThreadLocal.ThreadLocalMap в объекте текущего потока. не пустой, он будет основан на объекте ThreadLocal, который используется в качестве ключа для получения значения, соответствующего ключу; если переменная threadLocals равна NULL, инициализируется новый объект ThreadLocalMap.
Посмотрите на метод построения ThreadLocalMap, то есть в классе ThreadThreadLocal.ThreadLocalMap
Логика выполнения, когда свойство threadLocals типа не равно null.
// ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
Этот метод построения фактически использует объект ThreadLocal в качестве ключа и сохраненный конкретный объект содержимого в качестве значения, упаковывает его в объект Entry и помещает в атрибут таблицы массива Entry в классе ThreadLocalMap, тем самым завершая хранение потока. локальные переменные.
так,Данные в ThreadLocal в конечном итоге хранятся в классе ThreadLocalMap..
2.2 Метод хеширования
существуетThreadLocalMap#set(ThreadLocal<?> key, Object value)
В методе есть такая строчка кода:
// 获取当前 ThreadLocal 对象的散列值
int i = key.threadLocalHashCode & (len-1);
Значение, полученное этой строкой кода, на самом деле является хеш-значением объекта ThreadLocal, который является методом хеширования ThreadLocal, который мы называемХэш Фибоначчи.
// ThreadLocal#threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
// ThreadLocal#nextHashCode
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// ThreadLocal#nextHashCode
private static AtomicInteger nextHashCode = new AtomicInteger();
// AtomicInteger#getAndAdd
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 魔数 ThreadLocal#HASH_INCREMENT
private static final int HASH_INCREMENT = 0x61c88647;
key.threadLocalHashCode
Используемые функции и атрибуты показаны выше, а атрибут threadLocalHashCode каждого ThreadLocal основан на магическом числе.0x61c88647
генерировать.
Я не буду обсуждать здесь причину выбора этого магического числа (на самом деле я его не совсем понимаю), короче, много практики доказывает:использовать0x61c88647
ThreadLocalHashCode, сгенерированный как магическое число, делится на степень 2, и результат распределяется очень равномерно..
Выполните операцию остатка над A в степени двойки.
A % 2^N
в состоянии пройтиA & (2^n-1)
Вместо этого результаты вычислений для обоих одинаковы, а эффективность битовой операции намного выше, чем у модуля.Как много вы знаете о HashMap?Соответствующие рассуждения также были сделаны в статье.
2.3 Как разрешать коллизии хешей
Мы уже знаем, что базовая структура данных класса ThreadLocalMap представляет собой массив типа Entry, но в отличие от массива класса Node и формы связанного списка в HashMap, класс Entry не имеет следующего атрибута для формирования связанного списка, поэтому он простой массив.
даже если вышеХеширование ФибоначчиЕго действительно можно полностью хешировать, но коллизии хэшей все равно могут происходить, поэтому вопрос в том, как массив Entry разрешает коллизии хэшей?
Это требует выносаThreadLocal#set(T value)
метод, а конкретная логика для обработки хэш-коллизий находится вThreadLocalMap#set(ThreadLocal<?> key, Object value)
в методе:
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
ThreadLocalMap map = getMap(t);
// 若 threadLocals 变量不为空,进行赋值;否则新建一个 ThreadLocalMap 对象来存储
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {
// 获取 ThreadLocalMap 的 Entry 数组对象
Entry[] tab = table;
int len = tab.length;
// 基于斐波那契散列法获取当前 ThreadLocal 对象的散列值
int i = key.threadLocalHashCode & (len-1);
// 解决哈希冲突,线性探测法
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 代码(1)
if (k == key) {
e.value = value;
return;
}
// 代码(2)
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 代码(3)将 key-value 包装成 Entry 对象放在数组退出循环时的位置中
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// ThreadLocalMap#nextIndex
// Entry 数组的下一个索引,若超过数组大小则从0开始,相当于环形数组
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
Конкретный анализ и обработка хэш-коллизийThreadLocalMap#set(ThreadLocal<?> key, Object value)
можно увидеть, что после получения хеш-значения объекта ThreadLocal вводится цикл for, причем условия цикла также предельно ясны: начиная с хэш-значения объекта ThreadLocal в массиве Entry, двигаться назад на один бит каждый раз, если размер массива превышен, он будет продолжать проходить от 0 до тех пор, пока объект Entry не станет NULL.
Во время цикла:
-
Например, код (1), если текущий объект ThreadLocal точно равен атрибуту ключа в объекте Entry, напрямуюОбновить значение value в ThreadLocal;
-
Как показано в коде (2), если текущий объект ThreadLocal не равен атрибуту ключа в объекте Entry, а ключ объекта Entry пуст, логика здесь фактическиУстановите пары ключ-значение при очистке недопустимой записи(на основеДетективная уборкаПредотвратить утечку памяти в определенных программах, о чем будет подробно рассказано ниже);
-
Как показано в коде (3), если при обходе не найдено хэш-значение текущего объекта TheadLocal, а ключ объекта Entry не оказывается пустым, условие выхода из цикла выполняется (т. е. найдена пара ключ-значение, в которой может храниться эта пара ключ-значение (позиция), тоСоздайте новый объект Entry для хранения, то сделайте это один разЭвристическая очистка, в цикле while позиция нижнего индекса непрерывно перемещается вправо, чтобы очистить недопустимые элементы, и все значения значений объектов, чей ключ пуст, а значение не пусто в массиве Entry, выпущенный;
До сих пор мы анализировали логику после получения хеш-значения объекта ThreadLocal при хранении данных в ThreadLocal, и вернемся к теме этого раздела — как ThreadLocal разрешает хеш-конфликты?
Из приведенного выше кода видно, что цикл вводится после получения хеш-значения текущего объекта ThreadLocal на основе метода хеширования Фибоначчи.
- Если хэш-значение уже существует, а ключ — тот же объект, обновите значение напрямую.
- Если хэш-значение уже существует, но ключ не является тем же объектом, попробуйте сохранить его в следующем пустом месте.
Таким образом, резюмируя, как ThreadLocal обрабатывает коллизии хэшей: **Если во время установки возникает коллизия хэшей, ThreadLocal попытается сохранить ее в следующей позиции индекса массива посредством линейного обнаружения и в то же время решить ее на основе линейного обнаружения. В случае конфликта атрибут value недопустимого объекта Entry, ключ которого равен NULL и значение которого не равно NULL, также будет освобожден на основе проверки очистки для предотвращения утечек памяти**.
2.4 Механизм расширения
Давайте поговорим о механизме расширения ThreadLocal.Во-первых, давайте представим начальную емкость ThreadLocal. Конструктор ThreadLocalMap упоминался выше, а именно:
// ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化 Entry 数组
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置扩容条件
setThreshold(INITIAL_CAPACITY);
}
- Начальная емкость ThreadLocalMap составляет 16
// 初始化容量
private static final int INITIAL_CAPACITY = 16;
Давай поговоримМеханизм расширения ThreadLocalMap, у него есть два шага оценки перед расширением, и окончательное расширение будет выполнено только после того, как все будут удовлетворены:
-
ThreadLocalMap#set(ThreadLocal<?> key, Object value)
метод вызоветЭвристическая очистка, после эвристической очистки недопустимых объектов Entry, если длина массива больше или равна 2/3 заданной длины массива, сначала выполняется перехеширование;
// 启发式清理后,若容量大于等于阈值,触发 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
// rehash 条件 sz >= threshold
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
- перефразирование вызоветПолная очистка, если длина массива больше или равна 1/2 заданной длины массива после завершения очистки, будет выполнено расширение;
// 扩容条件
private void rehash() {
// 全量清理
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// threshold = len*2/3
// threshold - threshold*1/4 = len*2/3 - len*2/3*1/4 = 1/2
if (size >= threshold - threshold / 4)
resize();
}
- При расширении массив Entryв 2 раза больше первоначальной емкости, пересчитайте хеш-значение ключа. Если ключ снова равен NULL, его значение также будет установлено на NULL, чтобы помочь виртуальной машине выполнить сборку мусора.
// 具体的扩容函数
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 2倍扩容
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
2.5 Как локальные переменные передаются между родительским и дочерним потоками
Мы уже знаем, что локальные переменные потока хранятся в ThreadLocal, поэтому, если сейчас есть необходимость, как добиться переноса локальных переменных между потоками?
На самом деле большие ребята давно ожидали такого спроса, поэтому они разработали класс InheritableThreadLocal.
Исходный код класса InheritableThreadLocal не превышает 10 строк, кроме комментариев, потому что он наследуется от класса ThreadLocal, в классе ThreadLocal реализовано многое, а класс InheritableThreadLocal переписывает только три метода:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
Давайте используем простой пример, чтобы попрактиковаться в функции передачи локальных переменных между родительским и дочерним потоками.
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("这是父线程设置的值");
new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start();
}
// 输出内容
子线程输出:这是父线程设置的值
Как видите, в дочернем потоке вызовомInheritableThreadLocal#get()
метод для получения значения, установленного в родительском потоке.
Итак, как это достигается?
Реализация совместного использования локальных переменных между родительским и дочерним потоками должна быть прослежена до конструктора объекта Thread:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
// 该参数一般默认是 true
boolean inheritThreadLocals) {
// 省略大部分代码
Thread parent = currentThread();
// 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// 省略部分代码
}
В окончательном методе построения выполнения есть такое суждение: если свойство inheritableThreadLocals текущего родительского потока (потока, создающего дочерний поток) не равно NULL, свойство inheritableThreadLocals текущего родительского потока будет изменено.копироватьСвойство inheritableThreadLocals для дочерних потоков. Конкретный метод копирования выглядит следующим образом:
// ThreadLocal#createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 一个个复制父线程 ThreadLocalMap 中的数据
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// childValue 方法调用的是 InheritableThreadLocal#childValue(T parentValue)
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
Следует отметить, что время копирования общей переменной родительского потока наступает при создании дочернего потока.Если родительский поток устанавливает содержимое в объекте типа InheritableThreadLocal после создания дочернего потока, он больше не будет виден для дочерний поток (на основе новой записи (ключ). , значение) получить два разных объекта Entry).
3. Анализ утечки памяти ThreadLocal
Наконец, давайте поговорим о проблеме утечки памяти ThreadLocal.Как мы все знаем, при неправильном использовании ThreadLocal вызывает утечку памяти.
утечка памятиЭто означает, что динамически выделяемая куча памяти в программе не освобождается или не может быть освобождена по какой-либо причине, что приводит к пустой трате системной памяти, что приводит к серьезным последствиям, таким как замедление скорости работы программы или даже сбой системы.
3.1 Причины утечек памяти
Причина утечки памяти ThreadLocal должна начинаться с объекта Entry.
// ThreadLocal->ThreadLocalMap->Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Ключ объекта Entry, класс ThreadLocal, наследуется отWeakReference слабый ссылочный класс. Объекты со слабыми ссылками имеют меньшее время жизни,Объекты со слабыми ссылками удаляются сборщиком мусора, когда происходит активность GC, независимо от того, достаточно ли места в памяти..
Поскольку ключ объекта Entry наследуется от класса слабой ссылки WeakReference, если класс ThreadLocal не имеет внешней строгой ссылки, объект ThreadLocal будет повторно использован при выполнении действия GC.
В это время, если поток, создавший класс ThreadLocal, все еще активен, значение, соответствующее объекту ThreadLocal в объекте Entry, по-прежнему имеет строгую ссылку и не будет переработано, что приведет к утечке памяти.
3.2 Как решить проблему с утечкой памяти
Решить проблему утечки памяти на самом деле очень просто, просто не забудьте поместить содержимое, хранящееся в ThreadLocal, после его использования.removeПросто брось это.
Это способ активного предотвращения утечек памяти, но на самом деле великие боги, которые разработали ThreadLocal, также обнаружили, что ThreadLocal может вызывать утечки памяти, поэтому они также разработали соответствующие меры для предотвращения утечек памяти.
3.3 Как предотвратить утечку памяти внутри ThreadLocal
3.3.1 Метод установки
Сначала поговорим о методе set, который собственно описан вышеThreadLocalMap#set(ThreadLocal<?> key, Object value)
На самом деле уже есть логика, связанная с очисткой недействительной записи внутри ThreadLocal:
// ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 解决哈希冲突,线性探测法
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 代码(1):更新 Threadlocal 值
if (k == key) {
e.value = value;
return;
}
// 代码(2):探测式清理无效 Entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 代码(3):存放键值对,进行启发式清理
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 代码(4):rehash 中会进行全量清理
rehash();
}
-
прежде всегоДетективная уборка, расположение кода (2). При разрешении конфликта хэшей на основе метода линейного обнаружения, если ключ имеет значение null, будет вызван метод ThreadLocalMap#replaceStaleEntry для очистки недопустимой записи.
-
ПослеЭвристическая очистка: после того, как ThreadLocal разрешит коллизию хэшей, сохраните пару ключ-значение в местоположении после выхода из линейного обнаружения. Затем выполните эвристическую очистку.В цикле while позиция индекса непрерывно перемещается вправо для очистки недопустимых элементов.Следует отметить, что для вставки элементов требуется время O(n).
// 启发式清理:在 在 while 循环中将本次索引下标位置开始不断的右移清理无效的元素,需要注意这里会消耗 O(n) 的时间用于插入元素
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
- Наконец, если условие перефразирования выполнено, оно будет выполнено один раз в перефразировании.Полная очистка: очистить все недопустимые объекты Entry, ключ которых равен нулю и чье значение не равно нулю в цикле for
private void rehash() {
// 全量清理
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// 扩容
if (size >= threshold - threshold / 4)
resize();
}
// 全量清理
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
3.3.2 метод получения
В методе ThreadLocal#get также есть логика очистки от неверных записей, исходный код выглядит следующим образом:
// ThreadLocal#get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 实际获取 Entry 对象的方法
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// ThreadLocalMap#getEntry
private Entry getEntry(ThreadLocal<?> key) {
// 根据寻址算法计算 key 的索引值
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 若 key 对应索引值的 Entry 对象不为空,且键与当前 ThreadLocal 对象相同,返回该 Entry 对象
return e;
else
// 否则将按照解决哈希冲突时的线性探测法,向后遍历,尝试获取有效且 key 属性与当前 ThreadLocal 对象相同的 Entry 对象,同时若碰到无效 Entry 会进行清理
return getEntryAfterMiss(key, i, e);
}
Видно, что в методе ThreadLocalMap#getEntry процесс получения локальных переменных текущего потока в ThreadLocal таков:
- Сначала вычислите значение индекса ключа в соответствии с алгоритмом адресации.
- Если объект Entry, соответствующий значению индекса ключа, не пуст, и его ключевой атрибут совпадает с текущим объектом ThreadLocal, возвращается объект Entry (то есть возвращаются локальные переменные, сохраненные текущим потоком в ThreadLocal). )
- В противном случае он будет перемещаться назад в соответствии с методом линейного обнаружения при разрешении конфликтов хэшей и пытаться получить допустимый объект Entry с тем же атрибутом ключа, что и текущий объект ThreadLocal, и будет очищаться, если встретится недопустимая запись.
Давайте посмотрим на конкретную логику ThreadLocalMap#getEntryAfterMiss:
// ThreadLocalMap#getEntryAfterMiss
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 当 Entry 为空时结束循环
while (e != null) {
ThreadLocal<?> k = e.get();
// 若 Entry 对象的 key 与当前 ThreadLocal 相同,返回该 Entry 对象
if (k == key)
return e;
// 若 Entry 对象的 key 为空,进行清理
if (k == null)
expungeStaleEntry(i);
else
// 若 Entry 对象的 key 不为空,计算下一个索引值,继续 while 循环
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
ThreadLocalMap#getEntryAfterMiss — это цикл while, и конечным условием цикла является то, что объект Entry пуст, что означает, что текущий поток не хранит локальные переменные в ThreadLocal. До этого в цикле while у ThreadLocal была бы следующая логика при поиске соответствующей Entry:
- Если ключ объекта Entry совпадает с текущим ThreadLocal, вернуть объект Entry (то есть вернуть локальные переменные, сохраненные текущим потоком в ThreadLocal)
- Если ключ объекта Entry пуст, это недопустимый объект Entry с нулевым ключом, что может привести к утечке памяти. Для очистки будет выполнен метод ThreadLocalMap#expungeStaleEntry.
- Если ключ объекта Entry не пуст, вычислить следующее значение индекса и продолжить цикл while
Наконец, давайте взглянем на исходный код метода ThreadLocal#expungeStaleEntry, который фактически выполняет очистку от недействительной записи в ThreadLocal:
// ThreadLocalMap#expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 清理当前无效 Entry 对象
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
// 执行 rehash 直到遇到 Entry=null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// rehash 过程中若碰到 key=null 的情况,进行清理
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 真正的 rehash 过程
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
Конкретная логика очистки ThreadLocalMap#expungeStaleEntry:
- Сначала обнулите текущее недопустимое значение объекта Entry, затем обнулите недопустимый объект Entry.
- Затем выполняется цикл for, и конечным условием цикла for является то, что объект Entry имеет значение null.
- Выполните логику повторного хеширования.Во время процесса повторного хеширования, если встречается объект Entry с нулевым ключом, значение текущего недопустимого объекта Entry также будет установлено в значение null, а затем недопустимый объект Entry будет установлен в значение null
- Если это обычный объект Entry, выполните перефразирование и скорректируйте его значение индекса.
3.3.3 метод удаления
Точно так же в методе ThreadLocal#remove он также содержит логику активной очистки недопустимых объектов Entry для предотвращения утечек памяти.Давайте посмотрим непосредственно на код:
// ThreadLocal#remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// ThreadLocalMap#remove(ThreadLocal)
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 获取 key 的索引下标值
int i = key.threadLocalHashCode & (len-1);
// 循环进行无效 Entry 对象的清理,直到 Entry 为空
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 若 Entry 的 key 属性对应的正是当前 ThreadLocal 对象
if (e.get() == key) {
// 将 Entry 的 key 置为 null
e.clear();
// 实际执行清理 Entry 对象的方法
expungeStaleEntry(i);
return;
}
}
}
Метод ThreadLocal#remove при выполнении имеет следующую логику:
- Сначала вычислите значение индекса индекса ключа
- Выполните цикл for, и условием завершения цикла является то, что Entry пуста. Во время цикла, если обнаруживается, что ключевой атрибут Entry соответствует текущему объекту ThreadLocal, для ключа устанавливается значение null, и вызывается метод ThreadLocal#expungeStaleEntry для очистки значения.
3.3.4 Сводка по предотвращению утечки памяти внутри ThreadLocal
Подводя итог тому, как ThreadLocal предотвращает внутренние утечки памяти:
- существуетsetКогда элемент обнаружен, позиция индекса, подходящая для хранения объекта Entry, будет найдена на основе метода линейного обнаружения, иДетективная уборкаОчистите недопустимую запись от позиции индекса этого индекса до позиции, где следующая запись равна нулю. последуетЭвристическая очистка, в цикле while текущая позиция нижнего индекса индекса постоянно сдвигается вправо, чтобы очистить недопустимые элементы. Наконец, если условие перефразирования выполнено, оно будет выполнено один раз в перефразировании.Полная очистка: очистить все недопустимые объекты Entry в цикле for.
- существуетget/removeметод будет основан наДетективная уборкаОчистите недопустимую запись от позиции индекса этого индекса до позиции, где следующая запись равна нулю.
Из этого видно, что независимо от того, какой из set/get/remove берет на себя инициативу по очистке недействительной записи, он не может полностью избежать проблемы утечек памяти.Чтобы полностью решить проблему утечек памяти, необходимо развиватьАктивно вызывать метод удаления для освобождения ресурсов после использования.хорошие привычки.
4. Сценарии и примеры применения ThreadLocal
ThreadLocal имеет приложения во многих средах с открытым исходным кодом, таких как: реализация уровня изоляции транзакций в Spring, реализация плагина подкачки MyBatis PageHelper и т. д.
При этом у меня также есть функция аутентификации на основе ThreadLocal и фильтры для реализации белого списка интерфейсов в проекте.
5. Резюме
Наконец, резюмируйте то, что эта статья описывает о ThreadLocal в форме вопросов для интервью:
- Какие проблемы решает ThreadLocal
- Базовая структура данных ThreadLocal
- Хэш-метод ThreadLocalMap
- Как ThreadLocalMap обрабатывает коллизии хэшей
- Механизм расширения ThreadLocalMap
- Как ThreadLocal реализует совместное использование локальных переменных между родительским и дочерним потоками
- Почему ThreadLocal теряет память
- Как решить утечку памяти ThreadLocal
- Как предотвратить утечку памяти внутри ThreadLocal и какие существуют методы
- Сценарии приложения ThreadLocal
6. Ссылки
- Интерпретация исходного кода ThreadLocal
- Статья, подробно объясняющая проблему утечки памяти ThreadLocal из исходного кода.
Наконец, эта статья включена в личную базу знаний Юке:Backend технология, как я ее понимаю,Добро пожаловать.