1. Цели обучения
1. Каждый поток имеет объект ThreadLocalMap, каждый ThreadLocalMap содержит массив Entry[], а Entry состоит из ключа (threadLocal) и значения (данных).
2. Entry расширяет WeakReference, который является слабой ссылкой.Обратите внимание, что ключ (threadLocal) Entry является слабой ссылкой. Слабые ссылки собираются во время обычной сборки мусора.
3. Четыре наиболее часто используемых метода ThreadLocal:
- get()
- set()
- remove()
- initialValue()
В дополнение к ThreadLocal#initialValue(), другие методы будут вызывать ThreadLocal.ThreadLocalMap#expungeStaleEntry(), устанавливать запись с ключом == null и устанавливать ее значение в null, то есть очистку данных, чтобы облегчить сборку мусора GC .
Зачем делать такую уборку?
- Мы знаем, что объект Entry содержит ThreadLocal и значение, а ThreadLocal является референтом WeakReference (слабая ссылка). Каждый раз, когда период сборки мусора запускает GC, референт WeakReference будет утилизирован, а референту будет присвоено значение null. Тогда будет много Entry с threadLocal = null, но значение не будет пустым в массиве таблиц, наличие такой Entry не имеет реальной ценности.
4. Почему Entry наследует WeakReference?
- Избегайте утечек памяти: если используются сильные ссылки, ThreadLocal больше не упоминается в пользовательском процессе, но пока поток не завершается, в ThreadLocalMap все еще есть ссылки, которые не могут быть восстановлены сборщиком мусора, что приводит к утечкам памяти.
- Кроме того, при использовании технологии пула потоков, поскольку поток не будет уничтожен, после рециркуляции он будет повторно использован в следующий раз, что приведет к тому, что ThreadLocal не будет освобожден, и в конечном итоге приведет к утечкам памяти.
5. Какие есть ямки в ThreadLocal:
- Проблема с утечкой памяти:
Даже если ThreadLocal использует WeakReference (слабую ссылку), может возникнуть проблема с утечкой памяти, поскольку только ключ (то есть объект threadLocal) установлен в качестве слабой ссылки в объекте входа, а значение value — нет.По-прежнему будут следующие сильные зависимости:
Thread -> ThreaLocalMap -> Entry -> value
Как решить:
После использования ThreadLocal вручную вызовите ThreadLocal#remove(), который очистит ключ (то есть объект threadLocal) и значение в записи вместе. Если вызывается ThreadLocal#get(), ThreadLocal#set(T value), но они основаны на очистке данных, запускаемой после того, как сборщик мусора соберет ключ. Если возникает ситуация, когда сборщик мусора не собирает его своевременно, тоже проблема.
- Проблемы безопасности потоков:
Некоторые друзья могут подумать, что ThreadLocal не будет иметь проблемы безопасности потоков, на самом деле справа.Если мы определяем статическую переменную count, в случае многопоточности значение в threadLocal необходимо изменить и установить значение count, что также проблематично. Поскольку статические переменные совместно используются несколькими потоками, копии не будут сохраняться отдельно.
6. Каковы преимущества ThreadLocal по сравнению с синхронизированным и изменчивым?
- Синхронизированный: это нормально, когда объем параллелизма невелик.Если объем параллелизма велик, будет большое количество потоков, ожидающих блокировки одного и того же объекта, что приведет к резкому падению пропускной способности системы.
- volatile: измененные переменные не сохраняют копии и напрямую обращаются к основной памяти, в основном используется в сценарии одной записи и нескольких чтений
- ThreadLocal: создайте копию переменной для каждого потока, чтобы гарантировать, что каждый поток обращается к своей собственной копии и изолирует друг друга, поэтому не будет проблем с безопасностью потока.
Во-вторых, использование ThreadLocal
1. Поток небезопасен:
public class ThreadA extends Thread {
private int i;
private UnsafeThread unsafeThread;
ThreadA(int i, UnsafeThread unsafeThread) {
this.i = i;
this.unsafeThread = unsafeThread;
}
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
unsafeThread.calc();
System.out.println("i:" + i + ",count:" + unsafeThread.getCount());
}
}
public class UnsafeThread {
private int count = 0;
public void calc() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
UnsafeThread testThread = new UnsafeThread();
for (int i = 0; i < 20; i++) {
new ThreadA(i, testThread).start();
}
Thread.sleep(200);
System.out.println("realCount:" + testThread.getCount());
}
}
результат операции:
i:6,count:8
i:0,count:8
i:17,count:13
i:7,count:8
i:2,count:8
i:11,count:8
i:8,count:11
i:5,count:11
i:13,count:11
i:15,count:11
i:14,count:11
i:10,count:8
i:3,count:11
i:12,count:11
i:9,count:8
i:19,count:15
i:4,count:15
i:16,count:15
i:1,count:15
i:18,count:15
realCount:15
Мы видим: есть проблема безопасности потоков
- realCount наконец-то получил ошибку, ожидаемый результат должен быть 20, но реальная ситуация 15
- количество дублируется
2. Добавьте ThreadLocal, безопасность потоков:
class ThreadB extends Thread {
private int i;
private SafeThread testThreadLocal;
ThreadB(int i, SafeThread testThreadLocal) {
this.i = i;
this.testThreadLocal = testThreadLocal;
}
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
testThreadLocal.calc();
System.out.println("i:" + i + ",count:" + testThreadLocal.getCount());
}
}
public class SafeThread {
private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private int count = 0;
public void calc() {
threadLocal.set(count + 1);
}
public int getCount() {
Integer integer = threadLocal.get();
return integer != null ? integer : 0;
}
public static void main(String[] args) throws InterruptedException {
SafeThread testThreadLocal = new SafeThread();
for (int i = 0; i < 20; i++) {
new ThreadB(i, testThreadLocal).start();
}
Thread.sleep(200);
System.out.println("realCount:" + testThreadLocal.getCount());
}
}
результат операции:
i:1,count:1
i:10,count:1
i:4,count:1
i:11,count:1
i:6,count:1
i:7,count:1
i:9,count:1
i:3,count:1
i:12,count:1
i:0,count:1
i:5,count:1
i:8,count:1
i:2,count:1
i:13,count:1
i:17,count:1
i:18,count:1
i:15,count:1
i:19,count:1
i:16,count:1
i:14,count:1
realCount:0
В-третьих, принцип работы ThreadLocal
1. Поток небезопасен:
Мы видим, что несколько потоков могут одновременно обращаться к общему счетчику ресурсов.Когда поток выполняет count++, другие потоки также могут выполнять count++ одновременно. Однако из-за невидимости переменной count нескольких потоков другие потоки получат старое значение счетчика + 1, поэтому существует проблема с данными, из-за которой ожидается, что realCount будет равен 20, но на самом деле он равен 15.
2. Безопасность потока:
как показано на рисунке:
- В общем направлении ThreadLocal создаст копию переменной для каждого потока, гарантируя, что каждый поток будет обращаться к своей собственной копии и изолирован друг от друга.
- скажем, в маленьком направлении,Каждый внутренний поток имеет threadLocalMap, threadLocalMap, каждая запись которого содержит массив, и запись данных посредством threadLocal (упомянутая здесь как число).
Таким образом, каждый поток имеет свой собственный счетчик переменных.
В примере 2, когда поток 1 вызывает метод calc, он сначала вызовет метод getCount.Поскольку возвращаемое значение первого вызова threadLocal.get() пусто, возвращаемое значение getCount равно 0. Таким образом, threadLocal.set(getCount() + 1) становится threadLocal.set(0 + 1), что устанавливает значение данных threadLocal в потоке 1 равным 1.
Поток 2 снова вызывает метод calc, а также сначала вызывает метод getCount.Поскольку возвращаемое значение первого вызова threadLocal.get() пусто, возвращаемое значение getCount также равно 0. Таким образом, threadLocal.set(getCount() + 1) также установит значение данных threadLocal в потоке 2 равным 1.
......
Наконец, значение данных в threadLocal каждого потока равно 1.
Кроме того, почему realCount в примере 2 выводится как 0?
- Поскольку testThreadLocal.getCount() вызывается в основном потоке, изменения других потоков будут влиять только на их собственные копии, а не на исходные переменные.Начальное значение count равно 0, поэтому в конце оно по-прежнему равно 0.
В-четвертых, анализ исходного кода ThreadLocal
1. Класс Thread определяет переменную-член с именем threadLocals, тип которой — ThreadLocal.ThreadLocalMap. Очевидно, что ThreadLocalMap — это внутренний класс ThreadLocal, который проверяет то, что я нарисовал на картинке,Каждый поток имеет объект ThreadLocalMap.
Thread.класс:
ThreadLocal.ThreadLocalMap threadLocals = null;
2. Конструктор ThreadLocalMap
ThreadLocal.ThreadLocalMap:
static class ThreadLocalMap {
// Entry是WeakReference(弱引用)的子类,弱引用对象会在gc回收时被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// Entry 包含了 ThreadLocal 变量 和 Object的value
Entry(ThreadLocal<?> k, Object v) {
// ThreadLocal变量做为WeakReference的referen
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
// 数组,它的类型是Entry
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
...
}
3. ThreadLocal#получить():
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程中的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果可以查询到数据
if (map != null) {
//从ThreadLocalMap中获取entry对象,Entry 的 key 为 ThreadLocal
ThreadLocalMap.Entry e = map.getEntry(this);
//如果entry存在
if (e != null) {
@SuppressWarnings("unchecked")
//获取entry中的值
T result = (T)e.value;
//返回获取到的值
return result;
}
}
//调用初始化方法,返回null
return setInitialValue();
}
ThreadLocal # получить карту (т):
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
Собственно вызов переменной класса Thread:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal#getEntry (ключ ThreadLocal>):
private Entry getEntry(ThreadLocal<?> key) {
//threadLocalHashCode是key的hash值
//key.threadLocalHashCode & (table.length - 1),
//相当于threadLocalHashCode对table.length - 1的取余操作,
//这样可以保证数组的下表在0到table.length - 1之间。
int i = key.threadLocalHashCode & (table.length - 1);
//获取下标对应的entry
Entry e = table[i];
//如果entry不为空,并且从弱引用中获取到的值(threadLocal) 和 key相同
if (e != null && e.get() == key)
//返回获取到的entry
return e;
else
//如果没有获取到entry或者e.get()获取不到数据,则清理空数据
return getEntryAfterMiss(key, i, e);
}
запись является подклассом WeakReference, тогда метод e.get() вызовет: Refernt#get()
public T get() {
return this.referent;
}
Возвращается ссылка, представляющая собой объект threadLocal, переданный конструктором.
ThreadLocal#getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
Этот метод вызовет метод expungeStaleEntry, на котором мы сосредоточимся позже.
Threadlocal # setInitialValue ():
private T setInitialValue() {
//调用用户自定义的initialValue方法,默认值是null
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程中的ThreadLocalMap,跟之前一样
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap不为空,
if (map != null)
//则覆盖key为当前threadLocal的值
map.set(this, value);
else
//否则创建新的ThreadLocalMap
createMap(t, value);
//返回用户自定义的值
return value;
}
4. ThreadLocal#set():
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal.ThreadLocalMap#set (ключ ThreadLocal>, значение объекта):
private void set(ThreadLocal<?> key, Object value) {
//将table数组赋值给新数组tab
Entry[] tab = table;
//获取数组长度
int len = tab.length;
//跟之前一样计算数组中的下表
int i = key.threadLocalHashCode & (len-1);
//循环变量tab获取entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//获取entry中的threadLocal对象
ThreadLocal<?> k = e.get();
//如果threadLocal对象不为空,并且等于key
if (k == key) {
//覆盖已有数据
e.value = value;
//返回
return;
}
//如果threadLocal对象为空
if (k == null) {
//创建一个新的entry赋值给已有key
replaceStaleEntry(key, value, i);
return;
}
}
//如果key不在已有数据中,则创建一个新的entry
tab[i] = new Entry(key, value);
//长度+1
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
Метод replaceStaleEntry также вызывает метод expungeStaleEntry.
5. ThreadLocal#удалить():
public void remove() {
//还是那个套路,不过简化了一下
//先获取当前线程,再获取线程中的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
//如果ThreadLocalMap不为空
if (m != null)
//删除数据
m.remove(this);
}
ThreadLocal.ThreadLocalMap#remove (клавиша ThreadLocal>):
private void remove(ThreadLocal<?> key) {
//将table数组赋值给新数组tab
Entry[] tab = table;
//获取数组长度
int len = tab.length;
//跟之前一样计算数组中的下表
int i = key.threadLocalHashCode & (len-1);
//循环变量从下表i之后不为空的entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//如果可以获取到threadLocal并且值等于key
if (e.get() == key) {
//清空引用
e.clear();
//处理threadLocal为空但是value不为空的entry
expungeStaleEntry(i);
return;
}
}
}
Метод очистки также очень прост, достаточно установить ссылку на ноль, то есть очистить ссылку
public void clear() {
this.referent = null;
}
ThreadLocal#expungeStaleEntry(int staleSlot)
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//将位置staleSlot对应的entry中的value设置为null,有助于垃圾回收
tab[staleSlot].value = null;
//将位置staleSlot对应的entry设置为null,有助于垃圾回收
tab[staleSlot] = null;
//数组大小-1
size--;
Entry e;
int i;
//变量staleSlot之后entry不为空的数据
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//获取当前位置的entry中对应的threadLocal
ThreadLocal<?> k = e.get();
//threadLocal为空,说明是脏数据
if (k == null) {
//value设置为null,有助于垃圾回收
e.value = null;
//当前位置的entry设置为null
tab[i] = null;
//数组大小-1
size--;
} else {
//重新计算位置
int h = k.threadLocalHashCode & (len - 1);
//如果h和i不相等,说明存在hash冲突
//现在它前面的脏Entry被清理
//该Entry需要向前移动,防止下次get()或set()的时候
//再次因散列冲突而查找到null值
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
Метод сначала очищает грязную запись в текущей позиции, затем выполняет итерацию в обратном направлении, пока table[i]==null. Если в процессе обхода снова встретится грязная запись, она будет очищена.
Если он не встречается, он повторно изменит текущую обнаруженную запись.Если индекс h, полученный путем повторного хеширования, несовместим с текущим индексом i, это означает, что хеш-конфликт произошел, когда запись была помещена в массив Entry ( его позиция определяется повторным хэшированием. Хэш смещен назад), и теперь грязная запись перед ней была очищена, поэтому текущая запись должна двигаться вперед, чтобы заполнить пустую позицию. В противном случае при следующем вызове метода set() или get() для поиска Entry будет найдено нулевое значение до него.
Зачем делать такую уборку?
- Мы знаем, что объект Entry содержит threadLocal и значение, а threadLocal является референтом WeakReference (слабая ссылка). Каждый раз, когда период сборки мусора запускает GC, референт WeakReference будет переработан, а референт будет иметь значение null. Тогда будет много Entry с threadLocal = null, но значение не будет пустым в массиве таблиц, наличие такой Entry не имеет реальной ценности.
- Этот тип данных не может получить значение через getEntry, потому что он содержит суждение if (e != null && e.get() == key).
Зачем использовать WeakReference?
- Избегайте утечек памяти: если используются сильные ссылки, ThreadLocal больше не упоминается в пользовательском процессе, но пока поток не заканчивается, в ThreadLocalMap все еще есть ссылки, которые не могут быть переработаны сборщиком мусора, что приведет к утечкам памяти. .
- Кроме того, при использовании технологии пула потоков, поскольку поток не будет уничтожен, после рециркуляции он будет повторно использован в следующий раз, что приведет к тому, что ThreadLocal не будет освобожден, и в конечном итоге приведет к утечкам памяти.
Четыре, ThreadLocal, которые ямы
1. Проблема с утечкой памяти:
Даже если ThreadLocal использует WeakReference (слабую ссылку), может возникнуть проблема с утечкой памяти, поскольку только ключ (то есть объект threadLocal) установлен в качестве слабой ссылки в объекте входа, а значение value — нет.По-прежнему будут следующие сильные зависимости:
Thread -> ThreaLocalMap -> Entry -> value
Как решить:
После использования ThreadLocal вручную вызовите ThreadLocal#remove(), который очистит ключ (то есть объект threadLocal) и значение в записи вместе. Если вызывается ThreadLocal#get(), ThreadLocal#set(T value), но они основаны на очистке данных, запускаемой после того, как сборщик мусора соберет ключ. Если возникает ситуация, когда сборщик мусора не собирает его своевременно, тоже проблема.
2. Вопросы безопасности потоков:
Некоторые друзья могут подумать, что использование ThreadLocal не вызовет проблем с безопасностью потоков, но это не так.Если мы определяем статическую переменную count, в случае многопоточности значение в threadLocal необходимо изменить и установить значение count, что также проблематично. Поскольку статические переменные совместно используются несколькими потоками, копии не будут сохраняться отдельно.
В. Резюме:
1. Каждый поток имеет объект ThreadLocalMap, каждый ThreadLocalMap содержит массив Entry[], а Entry состоит из ключа (threadLocal) и значения (данных).
2. Entry расширяет WeakReference, который является слабой ссылкой.Обратите внимание, что ключ (threadLocal) Entry является слабой ссылкой. Слабые ссылки собираются во время обычной сборки мусора.
3. Четыре наиболее часто используемых метода ThreadLocal:
- get()
- set()
- remove()
- initialValue()
В дополнение к ThreadLocal#initialValue(), другие методы будут вызывать ThreadLocal.ThreadLocalMap#expungeStaleEntry(), устанавливать запись с ключом == null и устанавливать ее значение в null, то есть очистку данных, чтобы облегчить сборку мусора GC .
Зачем делать такую уборку?
- Мы знаем, что объект Entry содержит ThreadLocal и значение, а ThreadLocal является референтом WeakReference (слабая ссылка). Каждый раз, когда период сборки мусора запускает GC, референт WeakReference будет утилизирован, а референту будет присвоено значение null. Тогда будет много Entry с threadLocal = null, но значение не будет пустым в массиве таблиц, наличие такой Entry не имеет реальной ценности.
4. Почему Entry наследует WeakReference?
- Избегайте утечек памяти: если используются сильные ссылки, ThreadLocal больше не упоминается в пользовательском процессе, но пока поток не завершается, в ThreadLocalMap все еще есть ссылки, которые не могут быть восстановлены сборщиком мусора, что приводит к утечкам памяти.
- Кроме того, при использовании технологии пула потоков, поскольку поток не будет уничтожен, после рециркуляции он будет повторно использован в следующий раз, что приведет к тому, что ThreadLocal не будет освобожден, и в конечном итоге приведет к утечкам памяти.
5. Какие есть ямки в ThreadLocal:
- Проблема с утечкой памяти:
Даже если ThreadLocal использует WeakReference (слабую ссылку), может возникнуть проблема с утечкой памяти, поскольку только ключ (то есть объект threadLocal) установлен в качестве слабой ссылки в объекте входа, а значение value — нет.По-прежнему будут следующие сильные зависимости:
Thread -> ThreaLocalMap -> Entry -> value
Как решить:
После использования ThreadLocal вручную вызовите ThreadLocal#remove(), который очистит ключ (то есть объект threadLocal) и значение в записи вместе. Если вызывается ThreadLocal#get(), ThreadLocal#set(T value), но они основаны на очистке данных, запускаемой после того, как сборщик мусора соберет ключ. Если возникает ситуация, когда сборщик мусора не собирает его своевременно, тоже проблема.
- Вопросы безопасности потоков:
Некоторые друзья могут подумать, что использование ThreadLocal не вызовет проблем с безопасностью потоков, но это не так.Если мы определяем статическую переменную count, в случае многопоточности значение в threadLocal необходимо изменить и установить значение count, что также проблематично. Поскольку статические переменные совместно используются несколькими потоками, копии не будут сохраняться отдельно.
6. Каковы преимущества ThreadLocal по сравнению с синхронизированным и изменчивым?
- Синхронизированный: это нормально, когда объем параллелизма невелик.Если объем параллелизма велик, будет большое количество потоков, ожидающих блокировки одного и того же объекта, что приведет к резкому падению пропускной способности системы.
- volatile: измененные переменные не сохраняют копии и напрямую обращаются к основной памяти, в основном используется в сценарии одной записи и нескольких чтений
- ThreadLocal: создайте копию переменной для каждого потока, чтобы гарантировать, что каждый поток обращается к своей собственной копии и изолирует друг друга, поэтому не будет проблем с безопасностью потока.