Я думаю, что ThreadLocal - это то, с чем все более или менее знакомы. Когда я столкнулся с ThreadLocal, я подумал, что это потрясающе. Я читал много блогов в Интернете и читал несколько книг, но я всегда чувствовал, что есть препятствие. что я не мог преодолеть. , поэтому я всегда немного разбирался в ThreadLocal, но, к счастью, эта штука мало используется в реальных разработках, так что мне приходится с этим жить. Конечно, я сказал «не сильно используется», но для обычной бизнес-разработки верхнего уровня, на самом деле, ThreadLocal используется во многих фреймворках, а некоторые даже дополнительно улучшили ThreadLocal. Но ThreadLocal также является основой параллельного программирования, поэтому он действительно необходим и должен быть тщательно изучен. Сегодня мы внимательно рассмотрим ThreadLocal.
Простое приложение ThreadLocal
Мы знаем, что при многопоточности при работе с общей переменной легко вызвать конфликты.Для решения этой проблемы лучше всего, конечно, чтобы каждый поток имел свою собственную переменную, и другие потоки не могли получить к ней доступ. под названием «не делиться, просто не навредить». Итак, как это сделать? ThreadLocal такой великолепный.
Давайте сначала рассмотрим простое приложение:
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("Hello");
System.out.println("当前线程是:" + Thread.currentThread().getName());
System.out.println("在当前线程中获取:" + threadLocal.get());
new Thread(() -> System.out.println("现在线程是"+Thread.currentThread().getName()+"尝试获取:" + threadLocal.get())).start();
}
результат операции:
当前线程是:main
在当前线程中获取:Hello
现在线程是Thread-0尝试获取:null
Результат выполнения легко понять: если значение вставляется в threadLocal в основном потоке, значение может быть получено только в том же потоке, и значение не может быть получено в других потоках.
Попробуйте написать ThreadLocal самостоятельно
Прежде чем мы приступим к изучению ThreadLocal, давайте подумаем над вопросом: если бы вас попросили внедрить ThreadLocal, что бы вы сделали? Цель ThreadLocal — позволить каждому потоку иметь свои собственные переменные. Самый прямой способ - создать новый общий класс и определить карту в классе.Ключ имеет тип Long, который используется для хранения идентификатора потока, а значение имеет тип T, который используется для хранения конкретные данные.
-
Когда установлено, получить идентификатор текущего потока, использовать его в качестве ключа и вставить данные в карту;
-
При получении по-прежнему получайте идентификатор текущего потока, используйте его в качестве ключа, а затем извлекайте данные из карты.
Нравится:
public class ThreadLocalTest {
public static void main(String[] args) {
CodeBearThreadLocal threadLocal = new CodeBearThreadLocal();
threadLocal.set("Hello");
System.out.println("当前线程是:" + Thread.currentThread().getName());
System.out.println("在当前线程中获取:" + threadLocal.get());
new Thread(() -> System.out.println("现在线程是" + Thread.currentThread().getName() + "尝试获取:" + threadLocal.get())).start();
}
}
class CodeBearThreadLocal<T> {
private ConcurrentHashMap<Long , T> hashMap = new ConcurrentHashMap<>();
void set(T value) {
hashMap.put(Thread.currentThread().getId(),value);
}
T get() {
return hashMap.get(Thread.currentThread().getId());
}
}
результат операции:
当前线程是:main
在当前线程中获取:Hello
现在线程是Thread-0尝试获取:null
Вы можете видеть, что текущий результат точно такой же, как у "настоящего ThreadLocal".
Исследуйте ThreadLocal
Мы тоже сами написали ThreadLocal, и вроде бы проблем нет, функция реализована буквально в несколько строчек кода, и я себе аплодирую. Как реализован настоящий ThreadLocal? Ядро должно быть похоже на то, что мы написали. К сожалению, подлинный ThreadLocal полностью отличается от того, что мы написали.
Давайте теперь посмотрим, как это делает настоящий ThreadLocal.
set
public void set(T value) {
Thread t = Thread.currentThread();//获取当前的线程
ThreadLocalMap map = getMap(t);//获取ThreadLocalMap
if (map != null)//如果map不为null,调用set方法塞入值
map.set(this, value);
else
createMap(t, value);//新建map
}
- Получить текущий поток и назначить его t;
- Вызвать метод getMap, передать t, то есть передать текущий поток, получить ThreadLocalMap и присвоить его карте;
- Если карта не нулевая, вызовите метод set, чтобы вставить значение;
- Если карта имеет значение null, вызывается метод createMap.
Давайте посмотрим на метод getMap:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
Метод getMap относительно прост. Он напрямую возвращает threadLocals объекта входящего потока, указывая, что threadLocals определен в классе Thread и имеет тип ThreadLocalMap. Давайте посмотрим на определение threadLocals:
public class Thread implements Runnable{
ThreadLocal.ThreadLocalMap threadLocals = null;
}
Увидев это определение, у всех должно закружиться голова Мы пришли с методом set класса ThreadLocal.
Давайте упорядочим отношения, они действительно немного запутаны.Класс Thread определяет поле ThreadLocal.ThreadLocalMap.ThreadLocalMap — это внутренний статический класс TheadLocal, а Entry[] используется для сохранения данных. Это означает, что ThreadLocalMap в каждом экземпляре Thread уникален и не мешает друг другу. Подождите, разве это не демистифицирует ThreadLocal? Получается, что ThreadLocal делает это для того, чтобы у каждого потока была своя переменная.
Если вам непонятно, ничего страшного, мы углубимся в детали. В реализованном нами ThreadLocal мы используем карту для хранения данных. Ключ — это идентификатор потока. Вы можете понять, что ключ — это экземпляр потока, а значение — это данные, которые нам нужно сохранить. Когда мы вызываем метод get, мы используем идентификатор потока. , вы можете понять, что использование экземпляра Thread для извлечения данных из карты, так что данные, которые мы извлекаем, должны удерживаться этим потоком. Например, это поток A. Если вы передадите идентификатор потока B, то есть экземпляр Thread, переданный в поток B, определенно не сможет получить данные, хранящиеся в потоке A. Не должно быть никаких сомневаюсь в этом. Однако в подлинном ThreadLocal данные хранятся непосредственно в экземпляре Thread, так что данные каждого потока естественным образом изолированы.
Теперь мы решили проблему, как ThreadLocal достигает изоляции данных потока, но есть еще одна проблема, то есть я прочитал много блогов, когда начал изучать ThreadLocal, и до сих пор не могу понять. хранится в записи в ThreadLocalMap [], то это означает, что можно сохранить несколько данных, в противном случае можно использовать обычную переменную-член, зачем использовать массив? Но метод set, предоставляемый ThreadLocal, не перегружен. Если вы сначала установите «привет», а затем установите «пока», то «пока» обязательно перезапишет «привет». В отличие от HashMap, есть ключ и концепция значения. Эта проблема действительно беспокоила меня в течение долгого времени, и я, наконец, узнал причину позже.Мы можем создать несколько ThreadLocals, например:
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
threadLocal1.set("Hello");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("Bye");
}
Так что же происходит? Отпустите код набора еще раз, чтобы не пришлось долго листать вверх:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
И threadLocal1, и threadLocal2 вызывают метод set.Хотя threadLocal1 и threadLocal2 являются разными экземплярами, они находятся в одном потоке, поэтому ThreadLocalMap, полученный с помощью getMap, одинаков, что означает, что несколько данных сохраняются в одном и том же ThreadLocalMap.
В частности, как сохранить данные, этот код более сложный, включает слишком много деталей, я не очень хорошо его понимаю, знаю только общую идею, давайте посмотрим на определение Entry:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
Entry также является статическим внутренним классом ThreadLocalMap, который имеет только одно значение поля, что означает, что он отличается от HashMap и не имеет понятия связанного списка.
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();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- Назначьте таблицу на вкладку локальной переменной, эта таблица является полем, содержащим данные, и имеет тип Entry[];
- Получите длину вкладки и назначьте ее len;
- Найдите индекс i;
- Цикл for сначала получает значение e указанного индекса из вкладки по индексу, полученному на третьем шаге, если e==null, то он не войдет в этот цикл for, то есть если текущая позиция пуста, затем перейдите непосредственно к пятому шагу; если в текущем местоположении уже есть данные, определите, совпадает ли ThreadLocal в этом месте с тем, которое мы собираемся вставить, если да, замените его новым значением; если нет, найдите следующая вакансия;
- Поместите созданный экземпляр Entry на вкладку.
В ней очень много деталей, и читать ее немного запутанно, но самое главное надо понять.
get
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//传入当前线程,获取当前线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//传入ThreadLocal实例,获取Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;//返回值
}
}
return setInitialValue();
}
- получить текущий поток;
- Получить ThreadLocalMap текущего потока;
- Передайте экземпляр ThreadLocal и получите Entry;
- возвращаемое значение.
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
- Найдите индекс i;
- По индексу i взять значение из таблицы и присвоить его e;
- Если e не пусто, и экземпляр ThreadLocal, хранящийся в e, совпадает с переданным экземпляром ThreadLocal, возвращайтесь напрямую;
- Если e пусто или экземпляр ThreadLocal, удерживаемый e, и переданный экземпляр ThreadLocal не совпадают, продолжайте смотреть вниз.
краткое содержание
Метод set и метод get проанализированы, подведем небольшой итог. ThreadLocal, который мы используем снаружи, больше похож на класс инструмента, который сам не сохраняет никаких данных, но настоящие данные хранятся в экземпляре Thread, что, естественно, завершает изоляцию данных потока. Наконец, отправьте изображение, которое поможет вам лучше понять ThreadLocal:
утечка памяти
Давайте еще раз посмотрим на определение Entry:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry наследует WeakReference Что такое WeakReference не является предметом этой статьи, вы можете проверить это самостоятельно. WeakReference обертывает ThreadLocal, давайте посмотрим на метод построения Entry, вызовем super(k) и передаем экземпляр ThreadLocal, в который мы передали, то есть ThreadLocal сохраняется в объекте WeakReference. Это приводит к проблеме, когда ThreadLocal не имеет сильной зависимости, ThreadLocal будет переработан в следующей сборке мусора, ключ переработан, но значение не переработано, поэтому получается, что ключ в Entry[] равен NULL, но В случае элементов, значение которых не равно NULL, если вы хотите переработать, вы можете завершить жизненный цикл потока, создавшего ThreadLocal. Но в реальной разработке поток, скорее всего, будет жить и умирать вместе с программой.Пока программа не останавливается, поток продолжает прыгать. Поэтому после использования метода ThreadLocal лучше вручную вызвать метод удаления, например:
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal();
try {
threadLocal.set("Hello");
threadLocal.get();
} finally {
threadLocal.remove();
}
}
Не забывайте, что лучше добавить метод удаления напоследок.
InheritableThreadLocal
Вернемся к примеру в начале блога:
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("Hello");
System.out.println("当前线程是:" + Thread.currentThread().getName());
System.out.println("在当前线程中获取:" + threadLocal.get());
new Thread(() -> System.out.println("现在线程是" + Thread.currentThread().getName() + "尝试获取:" + threadLocal.get())).start();
}
результат операции:
当前线程是:main
在当前线程中获取:Hello
现在线程是Thread-0尝试获取:null
Новый поток после кода создается основным потоком, поэтому можно сказать, что этот поток является подпотоком основного потока.Значение ThreadLocal, установленное в основном потоке, не может быть получено в подпотоке, что легко понять, потому что это не один и тот же поток, но я хочу, чтобы дочерний поток наследовал данные в ThreadLocal основного потока. Появился InheritableThreadLocal, который может полностью удовлетворить такие потребности:
public static void main(String[] args) {
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("Hello");
System.out.println("当前线程是:" + Thread.currentThread().getName());
System.out.println("在当前线程中获取:" + threadLocal.get());
new Thread(() -> System.out.println("现在线程是" + Thread.currentThread().getName() + "尝试获取:" + threadLocal.get())).start();
}
результат операции:
当前线程是:main
在当前线程中获取:Hello
现在线程是Thread-0尝试获取:Hello
Это позволяет дочернему потоку наследовать данные ThreadLocal основного потока, а точнее, дочерний поток наследует данные ThreadLocal родительского потока.
Итак, как это делается? Еще посмотри код.
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);
}
}
InheritableThreadLocal наследует ThreadLocal и переписывает три метода. Когда мы вызываем набор InheritableThreadLocal в первый раз, мы вызываем метод createMap InheritableThreadLocal, который создает экземпляр ThreadLocalMap и назначает его inheritableThreadLocals. Где определен этот inheritableThreadLocals? Подобно threadLocals для ThreadLocal, он также определен в классе Thread. Когда мы снова вызовем метод set, будет вызван метод getMap класса InheritableThreadLocal, а возвращаемое значение также будет inheritedThreadLocals, то есть исходные threadLocals будут заменены.
Когда мы создаем поток, вызывается конструктор Thread:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
Метод init относительно длинный, и я скопировал только код, относящийся к проблеме, которую мы хотим исследовать:
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
- Получить текущий поток, который в данный момент является родительским потоком.
- Если inheritableThreadLocals родительского потока не пуст, перейдите к if. Конечно, это определенно не пусто. Как мы уже говорили выше, вызов метода set в InheritableThreadLocal напрямую управляет inheritableThreadLocals. Что делается в if, так это передать inheritableThreadLocals родительского потока, создать новый ThreadLocalMap и назначить Укажите inheritableThreadLocals экземпляра Thead, чтобы дочерний поток имел ThreadLocalMap родительского потока, который завершает наследование и передачу ThreadLocal.
На этом блог заканчивается, там еще много всего, но все очень важное, особенно причины ThreadLocal и причины утечек памяти и как их избежать.