Объясните ThreadLocal и InheritableThreadLocal

Java

Оригинальный адрес:Параллельное программирование на Java ③ — Подробное объяснение ThreadLocal и InheritableThreadLocal

Пожалуйста, укажите источник!

предисловие

Прошлые статьи:

После окончания предыдущей статьи в этой статье в основном рассказывается о ThreadLocal и InheritableThreadLocal. Основное содержание:

  • Принципы использования и реализации ThreadLocal
  • Побочные эффекты ThreadLocal
    • грязные данные
    • Анализ утечек памяти
  • Принципы использования и реализации InheritableThreadLocal

1. Локальный поток

ThreadLocal подходит для сценариев, где каждому потоку нужен собственный независимый экземпляр, и этот экземпляр необходимо использовать в нескольких методах, то есть переменные изолированы между потоками и совместно используются методами или классами.Если быть точным, ThreadLocal не предназначен специально для решения проблемы параллелизма, вызванной многопоточными общими переменными, но дает новую идею, спасая страну кривым путем.

Простая демонстрация использования ThreadLocal на примере кода.

public class ThreadLocalExample {

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {

ExecutorService service = Executors.newCachedThreadPool();

service.execute(() -> {
System.out.println(Thread.currentThread().getName() + " set 1");
threadLocal.set(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 不会收到线程2的影响,因为ThreadLocal 线程本地存储
System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
threadLocal.remove();
});

service.execute(() -> {
System.out.println(Thread.currentThread().getName() + " set 2");
threadLocal.set(2);
threadLocal.remove();
});

ThreadPoolUtil.tryReleasePool(service);
}
}

Как видите, поток 1 не будет затронут потоком 2, потому что ThreadLocal создает переменные, закрытые для потока.

2. Принцип реализации ThreadLocal ⭐

2.1 Уточнение связи между несколькими ключевыми классами в ThreadLocal

Давайте сначала посмотрим на диаграмму классов ThreadLocal и Thread, чтобы понять их основные методы и их связь друг с другом.

Обозначим на рисунке несколько классов:

  • Thread
  • ThreadLocal
  • ThreadLocalMap
  • ThreadLocalMap.Entry

Далее мы сначала начинаем понимать отношения между этими классами:

  1. В классе Thread есть переменная-член threadLocals (на самом деле существует еще и inheritableThreadLocals, о которой речь пойдет позже), а ее тип — внутренний статический класс ThreadLocalMap класса ThreadLocal.

    public class Thread implements Runnable {

    // ...... 省略

    /* ThreadLocal values pertaining to this thread. This map is maintained
    * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

  2. ThreadLocalMap — это настраиваемый Hashmap, почему именно HashMap? Хорошо известно, что каждый поток может быть связан с несколькими переменными ThreadLocal.

        /**
    * ThreadLocalMap is a customized hash map suitable only for
    * maintaining thread local values. No operations are exported
    * outside of the ThreadLocal class. The class is package private to
    * allow declaration of fields in class Thread. To help deal with
    * very large and long-lived usages, the hash table entries use
    * WeakReferences for keys. However, since reference queues are not
    * used, stale entries are guaranteed to be removed only when
    * the table starts running out of space.
    */
    static class ThreadLocalMap {
    // ...
    }
  3. При инициализации ThreadLocalMap создается массив Entry размером 16, а объект Entry также используется для хранения пар ключ-значение (этот ключ имеет фиксированный тип ThreadLocal). Стоит отметить, что эта запись наследуетWeakReference(Этот дизайн предназначен для предотвращения утечек памяти, которые будут обсуждаться позже)

            static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
    }
    }

2.2 Исходный код методов установки, получения и удаления ThreadLocal

a. void set(T value)

    public void set(T value) {
// ① 获取当前线程
Thread t = Thread.currentThread();
// ② 去查找对应线程的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// ③ 第一次调用就创建当前线程的对应的ThreadLocalMap
// 并且会将值保存进去,key是当前的threadLocal,value就是传进来的值
createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

b. T get()

    public T get() {
// ① 获取当前线程
Thread t = Thread.currentThread();
// ② 去查找对应线程的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
if (map != null) {
// ③ 不为null,返回当前threadLocal 对应的value值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// ④ 当前线程的threadLocalMap为空,初始化
return setInitialValue();
}

private T setInitialValue() {
// ⑤ 初始化的值为null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化当前线程的threadLocalMap
createMap(t, value);
return value;
}

protected T initialValue() {
return null;
}

c. void remove()

Если переменная threadLocals текущего потока не пуста, удалите локальную переменную, соответствующую указанному экземпляру ThreadLocal в текущем потоке.

	 public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

Как видно из исходного кода,От начала до конца эти локальные переменные хранятся не в экземпляре ThreadLocal, а в переменной threadLocals вызывающего потока, приватной threadLocalMap потока..

ThreadLocal — это инструментальная оболочка и ключ. Он помещает значение в threadLocals вызывающего потока через метод set и сохраняет его. Когда вызывающий поток вызывает свой метод get, оно используется из переменной threadLocals текущего потока.

На данный момент, даже если принцип реализации закончен, на самом деле исходный код ThreadLocal очень прост и понятен. Реальный фокус и сложность ThreadLocal — это содержание, стоящее за нами.

3. Побочные эффекты ThreadLocal

ThreadLocal разработан для того, чтобы потоки могли безопасно совместно использовать/передавать переменную, но у него есть определенные побочные эффекты.

Основная проблема с ThreadLocal заключается в том, что он производитгрязные данныеиутечка памяти.

Позвольте мне сначала сделать вывод.Эти две проблемы обычно вызваны использованием ThreadLocal в потоках пула потоков, потому что пул потоковповторное использование потокаирезидент памятидве особенности.

3.1 Грязные данные

Грязные данные должны быть понятны всем, поэтому давайте сначала уберем их. Мультиплексирование потоков создает грязные данные. Поскольку пул потоков будет повторно использовать объект Thread, переменная статического свойства ThreadLocal класса, связанного с Thread, также будет повторно использоваться. Если вы явно не вызываете remove() в теле метода run() реализованного потока для очистки информации ThreadLocal, относящейся к потоку, тогда можно получить() повторно используемый поток, если следующий поток не вызывает set( ), чтобы установить начальное значение Information, включая значение объекта потока, связанного с ThreadLocal.

Для простоты понимания вот демонстрация:

public class ThreadLocalDirtyDataDemo {

private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {

ExecutorService pool = Executors.newFixedThreadPool(1);

for (int i = 0; i < 2; i++) {
MyThread thread = new MyThread();
pool.execute(thread);
}
ThreadPoolUtil.tryReleasePool(pool);
}

private static class MyThread extends Thread {
private static boolean flag = true;

@Override
public void run() {
if (flag) {
// 第一个线程set之后,并没有进行remove
// 而第二个线程由于某种原因(这里是flag=false) 没有进行set操作
String sessionInfo = this.getName();
threadLocal.set(sessionInfo);
flag = false;
}
System.out.println(this.getName() + " 线程 是 " + threadLocal.get());
// 线程使用完threadLocal,要及时remove,这里是为了演示错误情况
}
}
}

Результаты:

Thread-0 线程 是 Thread-0
Thread-1 线程 是 Thread-0

3.2 Утечка памяти ⭐

Прежде чем говорить об этом, необходимо посмотреть на картинку, чтобы увидеть ссылочное отношение нескольких классов при использовании ThreadLocal с точки зрения стека и кучи.

Видите красную пунктирную стрелку? Это ключевой и сложный момент для понимания ThreadLocal.

Давайте еще раз посмотрим на исходный код Entry:

          static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

Каждая запись ThreadLocalMap представляет собой паруключСлабая ссылка -WeakReference<ThreadLocal<?>>, который начинается сsuper(k)можно увидеть. Кроме того, каждая запись содержит паруценностьсильные цитаты.

В предыдущем описании я упоминалEntry extends WeakReference<ThreadLocal<?>>заключается в предотвращении утечек памяти. Собственно, что здесь сказаноПредотвращение утечек памяти предназначено для объектов ThreadLocal..

как сказать? Продолжайте смотреть вниз.

Если вы изучали ссылки на Java, этоWeakReferenceНе должно быть незнакомым, когда JVM выполняет сборку мусора,Объекты, связанные только со слабыми ссылками, восстанавливаются независимо от того, достаточно ли памяти.

Для получения более подробной информации, пожалуйста, прочитайте эту статью автора[Красивая 4D-графика и тексты помогут вам освоить сборку мусора JVM #цитаты на Java]

Благодаря этому дизайну,Даже если поток выполняется, пока для ссылки на объект ThreadLocal установлено значение null, ключ записи будет автоматически удален сборщиком мусора в следующем YGC (поскольку только ThreadLocalMap имеет слабую ссылку на него, строгой ссылки нет).

Если ключевое значение Entry здесь является строгой ссылкой на объект ThreadLocal, тоДаже если ссылка на объект ThreadLocal объявлена ​​как null, эти ThreadLocal не могут быть переработаны, потому что все еще есть сильные ссылки из ThreadLocalMap, которые вызовут утечку памяти..

Такие ключи переработаны(key == null) Запись вызывается в исходном коде ThreadLocalMapstale entry(переводится как«Устаревшая запись»),При следующем выполнении методов getEntry и set для ThreadLocalMap значение этой устаревшей записи будет установлено равным нулю, так что переменная, на которую указывает исходное значение, может быть удалена сборщиком мусора..

«При следующем выполнении методов getEntry и set для ThreadLocalMap значение этих устаревших записей будет установлено равным нулю, так что переменная, на которую указывает исходное значение, может быть удалена сборщиком мусора». Эту часть описания можно найти вThreadLocalMap#expungeStaleEntry()Исходный код метода и место вызова метода.

Таким образом, ThreadLocalMap разработан таким образом,Исправлены возможные утечки памяти для объектов ThreadLocal.,И соответствующее значение также будет удалено сборщиком мусора из-за вышеупомянутого устаревшего механизма записи..


Но почему мы до сих пор говорим, что может быть проблема с утечкой памяти при использовании ThreadLocal?Здесь это означает, что проблема все еще существуетСлучай, когда экземпляр Value (фиолетовый блок на рисунке) не может быть переработан.

Обратите внимание, что предпосылка вышеописанного механизма заключается в том, что для ссылки ThreadLocal задано значение null, что активирует механизм слабой ссылки, а затем повторно использует экземпляр объекта Value записи. Давайте посмотрим на комментарии в исходном коде ThreadLocal.

instances are typically private static fields in classes

Объекты ThreadLocal часто используются как частные статические переменные.

-- Если ThreadLocal не является статическим и принадлежит классу экземпляра потока, он теряет существенное свойство совместного использования в потоках.

Если используется как статическая переменная, ее жизненный цикл не закончится, по крайней мере, с окончанием потока. То есть большинству статических объектов threadLocal никогда не присваивается значение null. В этом случае невозможно очистить экземпляр объекта Value через механизм устаревшей записи. Вы должны вручную удалить(), чтобы гарантировать это.

Вот пример выше.

public class ThreadLocalDirtyDataDemo {

private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {

ExecutorService pool = Executors.newFixedThreadPool(1);

for (int i = 0; i < 2; i++) {
MyThread thread = new MyThread();
pool.execute(thread);
}
ThreadPoolUtil.tryReleasePool(pool);
}

private static class MyThread extends Thread {
private static boolean flag = true;

@Override
public void run() {
if (flag) {
// 第一个线程set之后,并没有进行remove
// 而第二个线程由于某种原因(这里是flag=false) 没有进行set操作
String sessionInfo = this.getName();
threadLocal.set(sessionInfo);
flag = false;
}
System.out.println(this.getName() + " 线程 是 " + threadLocal.get());
// 线程使用完threadLocal,要及时remove,这里是为了演示错误情况
}
}
}

В этом примере, который, если не удалить () операцию, то после завершения выполнения потока объекты String через удерживаемые объекты ThreadLocal не будут освобождены.

Почему вы говорите, что мультиплексируется только поток? Конечно, поскольку эти локальные переменные хранятся во внутренних переменных потока, при уничтожении потока ссылка на объект ThreadlocalMap будет установлена ​​в NULL, объект экземпляра Value станет недостижимым объектом в памяти с уничтожением нить. , потом восстанавливается мусором.

    // Thread#exit()
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}

Суммировать

в заключении

  • Введение WeakReference заключается в том, чтобы спроектировать связь между объектами ThreadLocal и ThreadLocalMap как слабую ссылку, чтобы избежать проблемы с утечкой памяти, когда объекты экземпляра ThreadLocal не могут быть переработаны.Соответствующий объект экземпляра Value.
  • Проблема утечки памяти, о которой мы часто говорим, связана с экземпляром объекта Value, соответствующим threadLocal. Если объект потока используется повторно, а threadLocal является статической переменной, то при отсутствии функции remove() вручную это может привести к утечке памяти.
  • Вышеупомянутые два вида утечек памяти происходят только при повторном использовании потоков, потому что ссылка на объект threadLocalMap будет иметь значение null при уничтожении потока.
  • Решение побочных эффектов очень простое, то есть каждый раз, когда ThreadLocal используется, метод remove() должен быть вызван вовремя для очистки.

В-четвертых, InheritableThreadLocal

В некоторых сценариях дочерний поток должен иметь возможность получать локальные переменные родительского потока, например использовать единый идентификатор для отслеживания и записи ссылки вызова. Однако ThreadLocal не поддерживает наследование: после установки той же переменной ThreadLocal в родительском потоке соответствующий объект не может быть получен в дочернем потоке.

Для решения этой проблемы также появился InheritableThreadLocal.

4.1 Использование

public class InheritableThreadLocalDemo {

private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {
// 主线程
threadLocal.set("hello world");
// 启动子线程
Thread thread = new Thread(() -> {
// 子线程输出父线程的threadLocal 变量值
System.out.println("子线程: " + threadLocal.get());
});

thread.start();

System.out.println("main: " +threadLocal.get());

}
}

вывод:

main: hello world
子线程: hello world

4.2 Принцип

Чтобы понять принцип, давайте взглянем на исходный код 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 class Thread implements Runnable {

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Видно, что InheritableThreadLocal наследует ThreadLocal и переписывает три метода, кажется, что дорвей, который нужно реализовать, находится в этих трех методах.

Сначала посмотрите на код ③, InheritableThreadLocal переписывает метод createMap, так что теперь, когда метод set вызывается в первый раз, вместо threadLocals создается экземпляр переменной inheritableThreadLocals текущего потока. Из кода ② видно, что когда метод get вызывается для получения переменной карты внутри текущего потока, он получает inheritableThreadLocals вместо threadLocals.

Можно сказать, что в мире InheritableThreadLocal переменная inheritableThreadLocals заменяет threadLocals.

Код ② и ③ уже обсуждались, давайте посмотрим на код 1 и на то, как разрешить дочерним потокам доступ к локальным переменным родительского потока.

Это начинается с кода, который создает поток, и открывает конструктор по умолчанию класса Thread.Код выглядит следующим образом.

    public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {

// ... 省略无关部分
// 获取父线程 - 当前线程
Thread parent = currentThread();

// ... 省略无关部分
// 如果父线程的inheritThreadLocals不为null 且 inheritThreadLocals=true
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 设置子线程中的inheritableThreadLocals变量
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// ... 省略无关部分
}

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}

Давайте посмотрим, как внутри выполняется createInheritedMap.

        private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

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
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++;
}
}
}
}

Внутри конструктора скопируйте значение переменной-члена inheritableThreadLocals родительского потока в новый объект ThreadLocalMap.

резюме

В этой главе представлены соответствующие точки знаний о ThreadLocal и InheritableThreadLocal.

ThreadLocal реализует совместное использование переменных в потоках, а InheritableThreadLocal реализует наследование переменных между родительскими и дочерними потоками. Но есть еще один сценарий, который InheritableThreadLocal не может решить,Другими словами, в случае использования пулов потоков и других компонентов выполнения, которые будут объединять мультиплексированные потоки, задачи выполнения выполняются асинхронно, и необходимо передать контекст..

В ответ на описанную выше ситуацию Али открыл исходный кодTTLбиблиотека, то есть Transmittable ThreadLocal для решения этой проблемы, заинтересованные друзья могут пойти и посмотреть.

Когда будет время, напишу об этом отдельную статью.

Если эта статья была вам полезна, я надеюсь, что вы можете поставить лайк, это самая большая мотивация для меня 🤝🤝🤗🤗.

Ссылаться на

  • Красота параллельного программирования на Java
  • «Эффективность кода»