Как реализуется Java ThreadLocal под капотом?

Как реализуется ThreadLocal? Является ли он реализован в Java (используя некоторую параллельную карту от ThreadID к объекту) или использует какой-нибудь JVM-крючок для более эффективного использования?

Ответ 1

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

Наивная реализация

Если бы я попросил вас реализовать класс ThreadLocal<T> с учетом API, описанного в javadoc, что бы вы сделали? Первоначальная реализация, скорее всего, будет ConcurrentHashMap<Thread,T> с использованием Thread.currentThread() в качестве ее ключа. Это будет работать достаточно хорошо, но имеет некоторые недостатки.

  • Конкуренция по теме - ConcurrentHashMap - довольно умный класс, но в конечном итоге он все равно должен иметь дело с предотвращением чередования нитей от него любым способом, и если разные потоки ударяют его регулярно, произойдет замедление.
  • Постоянно сохраняет указатель как на Thread, так и на объект, даже после завершения Thread и может быть GC'ed.

Реализация, совместимая с GC

Повторите попытку, давайте рассмотрим проблему сбора мусора, используя слабые ссылки. Работа с WeakReferences может быть запутанной, но ее должно быть достаточно, чтобы использовать построенную таким образом карту:

 Collections.synchronizedMap(new WeakHashMap<Thread, T>())

Или, если мы используем Guava (и мы должны быть!):

new MapMaker().weakKeys().makeMap()

Это означает, что после того, как никто не удерживает Thread (подразумевая, что он закончен), ключ/значение может быть собрано в мусор, что является улучшением, но все же не затрагивает проблему конфликта нитей, что означает, что наш ThreadLocal не все, что удивительно класс. Кроме того, если кто-то решил удержать объекты Thread после того, как они закончили, они никогда не будут GC'ed, и поэтому и наши объекты, даже если они технически недоступны, теперь не будут.

Умная реализация

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

Реализация будет выглядеть примерно так:

// called for each thread, and updated by the ThreadLocal instance
new WeakHashMap<ThreadLocal,T>()

Здесь нет необходимости беспокоиться о concurrency, потому что только один поток будет когда-либо получать доступ к этой карте.

У разработчиков Java есть главное преимущество над нами: они могут напрямую развивать класс Thread и добавлять к нему поля и операции, и именно то, что они сделали.

В java.lang.Thread есть следующие строки:

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

Который, как предлагает комментарий, действительно является пакетно-частным отображением всех значений, отслеживаемых ThreadLocal объектами для этого Thread. Реализация ThreadLocalMap не является WeakHashMap, но она соответствует одному и тому же основному контракту, в том числе удерживая его ключи по слабой ссылке.

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();
}

И ThreadLocal.setInitialValue() вот так:

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

По существу, используйте карту в этом потоке, чтобы удерживать все наши объекты ThreadLocal. Таким образом, нам не нужно беспокоиться о значениях в других Threads (ThreadLocal буквально может обращаться только к значениям в текущем потоке) и поэтому не имеет проблем concurrency. Кроме того, как только Thread будет завершено, его карта автоматически будет GC'ed, и все локальные объекты будут очищены. Даже если Thread удерживается, объекты ThreadLocal сохраняются слабым эталоном и могут быть очищены, как только объект ThreadLocal выходит за пределы области видимости.


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

tl; dr ThreadLocal реализация довольно крутая и намного быстрее/умнее, чем вы могли бы подумать на первый взгляд.

Если вам понравился этот ответ, вы также можете оценить мой (менее подробный) обсуждение ThreadLocalRandom.

Thread/ThreadLocal фрагменты кода, взятые из реализация Oracle/OpenJDK Java 8.

Ответ 2

Вы имеете в виду java.lang.ThreadLocal. Это довольно просто, действительно, это всего лишь карта пар имя-значение, хранящаяся внутри каждого объекта Thread (см. Поле Thread.threadLocals). API скрывает эту деталь реализации, но это более или менее все, что есть.

Ответ 3

Переменные ThreadLocal в Java работают, обращаясь к HashMap, хранящемуся в экземпляре Thread.currentThread().

Ответ 4

Предположим, что вы собираетесь реализовать ThreadLocal, как вы делаете его конкретным потоком? Конечно, самый простой способ - создать нестатическое поле в классе Thread, назовем его threadLocals. Поскольку каждый поток представлен экземпляром потока, поэтому threadLocals в каждом потоке тоже будет отличаться. И это также то, что делает Java:

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

Что здесь ThreadLocal.ThreadLocalMap? Потому что у вас есть только threadLocals для потока, поэтому, если вы просто возьмете threadLocals в качестве своего ThreadLocal (скажем, определите threadLocals как Integer), у вас будет только один ThreadLocal для определенного потока. Что делать, если вам нужно несколько переменных ThreadLocal для потока? Самый простой способ - сделать threadLocals a HashMap, key каждой записи - это имя переменной ThreadLocal, а value каждой записи - значение переменной ThreadLocal. Немного смущает? Пусть говорят, что у нас есть два потока, t1 и t2. они принимают тот же экземпляр Runnable как параметр конструктора Thread, и оба они имеют две переменные ThreadLocal с именем tlA и tlb. Это то, что вам нравится.

t1.tlA

+-----+-------+
| Key | Value |
+-----+-------+
| tlA |     0 |
| tlB |     1 |
+-----+-------+

t2.tlB

+-----+-------+
| Key | Value |
+-----+-------+
| tlA |     2 |
| tlB |     3 |
+-----+-------+

Обратите внимание, что значения составлены мной.

Теперь это кажется идеальным. Но что такое ThreadLocal.ThreadLocalMap? Почему он просто не использовал HashMap? Чтобы решить проблему, давайте посмотрим, что произойдет, когда мы установим значение через метод set(T value) класса ThreadLocal:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

getMap(t) просто возвращает t.threadLocals. Поскольку t.threadLocals был инициализирован до null, поэтому сначала вводим createMap(t, value):

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

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

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

    ...

    /**
     * Construct a new map initially containing (firstKey, firstValue).
     * ThreadLocalMaps are constructed lazily, so we only create
     * one when we have at least one entry to put in it.
     */
    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);
    }

    ...

}

Основной частью класса ThreadLocalMap является Entry class, который расширяет WeakReference. Это гарантирует, что если текущая нить выйдет, это будет автоматически собираться мусором. Вот почему он использует ThreadLocalMap вместо простого HashMap. Он передает текущий ThreadLocal и его значение как параметр класса Entry, поэтому, когда мы хотим получить это значение, мы можем получить его из table, который является экземпляром класса Entry:

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();
}

Это то, что походит на всю картину:

Целое изображение

Ответ 5

Концептуально вы можете думать о ThreadLocal<T> как о Map<Thread,T>, который хранит значения, специфичные для потока, хотя это не так, как это фактически реализовано.

Значения, специфичные для потока, сохраняются в самом объекте Thread; когда поток завершается, значения потока могут быть собраны мусором.

Ссылка: JCIP