Раскрытие ThreadLocal

Java

Когда я впервые использовал C#, я изучал его ThreadLocal и LogicalCallContext (ExecutionContext), которые можно передавать между потоками.К сожалению, C# не является открытым исходным кодом (к счастью, у него есть .Net Core), поэтому я могу найти только документы и блоги повсюду. мир. После перехода на Java я, наконец, соприкоснулся с другим методом исследования проблем: вместо поиска информации я могу смотреть на код и отлаживать код. Тогда все менее загадочно.

Функция и основной принцип

На мой взгляд, Thread Local в основном предоставляет две функции:

  1. Легко передать параметры. Обеспечьте удобную «полку», храните ее как хотите и получайте, когда захотите, не передавая множество параметров для каждого вызова метода. (Обычно мы склонны откладывать общедоступные данные на полку)
  2. Изоляция нити. Значения каждого потока не зависят друг от друга, что избавляет от проблем многопоточности.

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)

Код хорошо прокомментирован. ThreadLocal следует переводить как [локальная переменная потока], что означает, что она относится к обычным переменным. ThreadLocal обычно является статической переменной, но ееget()Полученные значения не зависят друг от друга в потоках.

Несколько основных методов ThreadLocal:

  • get()Получить значение переменной. Возвращает значение, если этот ThreadLocal был установлен в текущем потоке; в противном случае вызывает косвенноinitialValue()Инициализировать переменную в текущем потоке и вернуть начальное значение.
  • set()Устанавливает значение переменной в текущем потоке.
  • protected initialValue()метод инициализации. Реализация по умолчанию должна возвращать null.
  • remove()Удалить переменную в текущем потоке.

Краткое описание принципа

  • Каждый поток имеет атрибут threadLocals типа ThreadLocalMap.Класс ThreadLocalMap эквивалентен Map, ключ — это сам ThreadLocal, а значение — это значение, которое мы установили.
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
  • когда мы проходимthreadLocal.set("xxx");Когда придет время, нужно поместить пару ключ-значение в свойство threadLocals этого потока, ключ — это текущий поток, а значение — заданное вами значение.
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.get(), мы используем текущий поток в качестве ключа для получения значения, установленного этим потоком.
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
             @SuppressWarnings("unchecked")
             T result = (T)e.value;
             return result;
        }
    }
    return setInitialValue();
}

thread-local.png

Ядро: ThreadLocalMap

ThreadLocalMap is a customized hash map suitable only for maintaining thread local values. 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.

ThreadLocalMap — это настраиваемая хэш-карта, использующая открытую адресацию для разрешения конфликтов.

  • Его вход представляет собойWeakReference, если быть точным, досталось по наследствуWeakReference
  • Ссылка на объект ThreadLocal передается вWeakReferenceизreferenceсередина,entry.get()Он используется как ключ элемента карты, а Entry имеет еще одно полеvalue, используемый для хранения фактического значения переменной ThreadLocal.
  • Поскольку это слабая ссылка, если объект ThreadLocal больше не имеет нормальной ссылки, объект ThreadLocal будет очищен при выполнении GC. И ключ Входа, а именноentry.get()станет нулевым. Однако GC очистит только указанный объект,Entry还被线程的ThreadLocalMap引用着,因而不会被清除。 таким образом,valueОбъект не будет очищен. Если поток не завершится, что приведет к освобождению ThreadLocalMap потока в целом, в противном случаеvalueпамять не может быть освобождена,утечка памяти!
  • Об этом, естественно, подумал автор JDK, поэтому во многих методах ThreadLocalMap вызовexpungeStaleEntries()Чистоentry.get() == nullэлемент, освободите значение Entry. Поэтому, пока поток все еще использует другие ThreadLocal, недействительная память ThreadLocal будет очищена.
  • Однако, в большинстве наших сценариев использования ThreadLocal являетсястатическая переменная, поэтому всегда есть нормальная ссылка на эту запись ThreadLocalMap в каждом потоке. Таким образом, запись ThreadLocal никогда не будет выпущена, естественноexpungeStaleEntries()бессилен,valueПамять тоже не освобождается. Поэтому после того, как у нас закончится ThreadLocal, мы можем активно вызыватьremove()способ активного удаления записи.

Тем не менее, действительно необходимо позвонитьremove()метод? Обычно наш сценарий — сервер, поток постоянно обрабатывает запросы, каждый запрос приводит к тому, что переменной Thread Local в потоке присваивается новое значение, а исходный объект значения естественным образом теряет свою ссылку и очищается сборщиком мусора. Поэтому при использовании статического локального потока и не установке для него значения null,нет утечки!

проходить через потоки

Thread Local не может передаваться между потоками, изоляция потоков! Но есть некоторые сценарии, где мы хотим пройти. Например:

  1. Начните новую тему, чтобы выполнить метод, но вы хотите, чтобы новый нить получить контекст (например, идентификатор пользователя, идентификатор транзакции) текущей резьбы через локальную резьбу.
  2. При отправке задач для выполнения пула потоков поток надеется, что будущие назначения также могут наследовать текущий поток Thread Local, поэтому вы можете использовать текущий контекст.

Давайте посмотрим на методы ниже.

InheritableThreadLocal

Принцип: InheritableThreadLocal Этот класс наследует ThreadLocal и переписывает 3 метода.

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 可以忽略
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

можно увидеть с помощьюInheritableThreadLocalКогда карта использует потокinheritableThreadLocalsполе вместо предыдущегоthreadLocalsполе.

Thread的两个字段及注释

а такжеinheritableThreadLocalsПоскольку поле называется наследуемым, оно естественно передается при создании нового потока. код в потокеinit()В методе:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

До сих пор с помощью inheritableThreadLocals мы могли передавать значение в ThreadLocal дочернему потоку, когда родительский поток создает дочерний поток Эта функция удовлетворяла большинство потребностей [1]. Но все еще существует серьезная проблема в случае повторного использования потока [2], такая как использование inheritableThreadLocals в пуле потоков для передачи значений, потому что inheritableThreadLocals передает значения только при создании нового потока, а повторное использование потока не выигрывает' не делай этого.

Здесь JDK ничего не может сделать. C# предоставляет LogicalCallContext (и механизм Execution Context) для решения этой проблемы.Чтобы решить эту проблему, Java должна сама расширить класс потока, чтобы реализовать эту функцию.

Alibaba с открытым исходным кодом, передаваемый в потоке, локальный

Адрес GitHub.

Существует три способа использования передаваемого локального потока: (режим декоратора!)

  1. Украсьте Runnable и Callable
  2. декорированный пул ниток
  3. Агент Java для оформления (изменения во время выполнения) класса реализации пула потоков JDK.

Официальная документация для конкретного использования очень ясна.

Ниже приводится краткий анализ принципа:

  • Поскольку мы хотим решить проблему локальной передачи потока при использовании пула потоков, мы должны передать текущее значение ThreadLocal, когда задача отправляется в поток при выполнении задачи.
  • И как пройти, естественно, захват (захват) перед подачей заданияВсе ThreadLocals текущего потока, сохраненные, а затем в целевом потоке, когда задача фактически выполняетсяОсвобождение (воспроизведение)** ранее захваченного ThreadLocal.

уровень кода для украшенияRunnableПример:

  1. При создании TtlRunnable() обязательно вызовите ее первой.capture()Захватите ThreadLocal в текущем потоке
private TtlCallable(@Nonnull Callable<V> callable, boolean releaseTtlValueReferenceAfterCall) {
    this.capturedRef = new AtomicReference<Object>(capture());
    ...
}
  1. capture()путьTransmitterСтатический метод класса:
public static Object capture() {
        Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
        for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
            captured.put(threadLocal, threadLocal.copyValue());
        }
        return captured;
}
  1. существуетrun(), сначала освободите ранее захваченный ThreadLocal.
public void run() {
    Object captured = capturedRef.get();
    ...
    Object backup = replay(captured);
    try {
        runnable.run();
    } finally {
        restore(backup); 
    }
}

Временная диаграмма:

完整时序图

заявление

  • Статические классы для Spring MVCRequestContextHolder,getRequestAttributes()На самом деле, то, что вы получаете,InheritableThreadLocal<RequestAttributes>значение в текущем потоке. Также можно сказать, что он может быть передан потокам, созданным им самим, но ничего не может сделать с существующими потоками.

    Что касается того, что он установлен, вы можете обратиться к его комментариям: Класс держателя для предоставления веб-запроса в виде связанного с потоком объекта RequestAttributes.Запрос будет унаследован любыми дочерними потоками.spawned by the current thread if the inheritable flag is set to true. Use RequestContextListener or org.springframework.web.filter.RequestContextFilter to expose the current web request. Note that org.springframework.web.servlet.DispatcherServlet already exposes the current request by default.

  • Подключение к базе данных в Spring, сессия в Hibernate.

  • Несколько сценариев применения, обобщенные Alibaba TTL

...

некоторые ямы

Хорошая статья:Расскажите о дизайне и недостатках ThreadLocal

  • Упомянул две проблемы, которые необходимо учитывать при разработке ThreadLocal, и то, как решается ThreadLocal.
    1. Когда поток завершается, как освободить ресурсы, чтобы избежать проблем с утечкой памяти.
    2. Доступ к данным карты может осуществляться несколькими потоками, может иметь место конкуренция за ресурсы, и необходимо учитывать проблемы параллельной синхронизации.
  • Упоминается, что TheadLocal является gc, но связанная с ним запись по-прежнему вызывает утечку памяти, что решается в Lucene:
    1. Когда я вижу ThreadLocal, я также думаю, что ключ теперь EntryWeakReference, почему не производится и Value?WeakReference, не слил?
    2. С другой стороны, если значение является слабой ссылкой, нет никакой гарантии, что оно все еще будет там, когда оно используется, потому что оно будет отброшено сборщиком мусора.
    3. Чтобы решить вышеуказанную проблему, Lucene сохранила еще однуWeakHashMap<Thread, T>, поэтому, пока поток все еще существует, значение не будет очищено.
    4. Однако это создает проблему многопоточного доступа, требующего блокировки.

Видите ли, не все вещи идеальны, мы осваиваем баланс.