Углубленный анализ исходного кода ThreadLocal

исходный код

Роль ThreadLocal

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

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

Глядя на официальную документацию, мы знаем, что ThreadLocal содержит следующие методы:

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

пример кода

public class Main {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return Integer.valueOf(0);
        }
    };

    public static void main(String[] args) {
        Thread[] threads = new Thread[5];
        for (int i=0;i<5;i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread() +"'s initial value: " + threadLocal.get());
                    for (int j=0;j<10;j++) {
                        threadLocal.set(threadLocal.get() + j);
                    }
                    System.out.println(Thread.currentThread() +"'s last value: " + threadLocal.get());
                }
            });
        }

        for (Thread t: threads)
            t.start();
    }
}

В приведенном выше примере threadLocal уникален для каждого потока, поэтому даже после накопления значения друг друга не будут влиять друг на друга.Окончательный вывод выглядит следующим образом:

Анализ исходного кода ThreadLocal

Прочитав основы использования ThreadLocal, читатели должны захотеть узнать, как ThreadLocal реализуется внутри? Основное содержание этой статьи — анализ внутренней реализации ThreadLocal на основе кода Java 1.8. Сначала посмотрите на картинку ниже:

Мы видим, что каждый поток поддерживаетThreadLocalMap, хранящийся в ThreadLocalMap, представляет собой массив таблиц с Entry в качестве элемента, Entry представляет собой структуру ключ-значение, ключ — это ThreadLocal, значение — это сохраненное значение. По аналогии с реализацией HashMap, каждый поток использует хеш-таблицу для хранения независимых от потока значений. Мы можем посмотреть на определение Entry:

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

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

Линия между ThreadLocal и ключом здесь — пунктирная линия, потому что Entry реализуется путем наследования WeakReference.Когда ThreadLocal Ref уничтожается, единственная сильная ссылка на экземпляр ThreadLocal в куче исчезает, и только Entry имеет слабую ссылку на экземпляр ThreadLocal. Предполагая, что вы знаете характеристики слабых ссылок, экземпляр ThreadLocal здесь может быть подвергнут сборке мусора. В это время ключ в Entry равен null, поэтому значение в Entry не может быть восстановлено до завершения потока.Возможна утечка памяти, о том, как ее решить, мы поговорим позже.

Зная приблизительную структуру данных, давайте рассмотрим реализацию нескольких конкретных методов ThreadLocal:

получить метод

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

Глядя непосредственно на код, можно проанализировать основные шаги следующим образом:

  1. Получите текущий объект Thread и получите ThreadLocalMap в Thread через getMap
  2. Если карта уже существует, используйте текущий ThreadLocal в качестве ключа, получите объект Entry и получите значение из Entry.
  3. В противном случае вызовите setInitialValue для инициализации.

Давайте посмотрим на функции, упомянутые выше:

getMap

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

getMap очень прост, он просто возвращает ThreadLocalMap в потоке и переходит к исходному коду потока, чтобы увидеть, что ThreadLocalMap определяется следующим образом:

ThreadLocal.ThreadLocalMap threadLocals = null;

Таким образом, ThreadLocalMap по-прежнему определен в ThreadLocal. Мы уже упоминали определение Entry в ThreadLocalMap. Чтобы сначала представить определение ThreadLocalMap, мы поместили setInitialValue впереди.

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

setInititialValue вызывается, когда карта не существует

  1. Первый — вызвать initialValue для генерации начального значения, углубиться в функцию initialValue, мы знаем, что она возвращает null;
  2. Затем получите следующую карту, если карта существует, непосредственно map.set, об этой функции будет сказано позже;
  3. Если он не существует, для создания ThreadLocalMap будет вызван метод createMap.Здесь можно сначала объяснить ThreadLocalMap.

ThreadLocalMap

Определение метода createMap простое:

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

Это вызов конструктора ThreadLocalMap для создания карты Давайте посмотрим на определение ThreadLocalMap:

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

    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0

    /**
     * Set the resize threshold to maintain at worst a 2/3 load factor.
     */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

    /**
     * Increment i modulo len.
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    /**
     * Decrement i modulo len.
     */
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

ThreadLocalMap определяется как статический класс с основными членами, которые содержат:

  1. Во-первых, это определение Входа, которое было сказано ранее;
  2. Начальная емкостьINITIAL_CAPACITY = 16;
  3. Основная структура данных — это таблица-массив Entry;
  4. размер используется для записи фактического количества записей на карте;
  5. Порог — это верхний предел расширения. Когда размер достигает порога, необходимо изменить размер всей Карты. Начальное значение порога равноlen * 2 / 3;
  6. nextIndex и prevIndex используются для безопасного перемещения индекса и часто используются в следующих функциях.

Конструктор ThreadLocalMap выглядит следующим образом:

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

Это использование firstKey и firstValue для создания записи, вычисление индекса i, затем вставка созданной записи в позицию i в таблице, а затем установка размера и порога.

Наконец, функция getEntry, которая завершает основную функцию функции get:

map.getEntry

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);
}
  1. Первый заключается в вычислении позиции индекса i, которая получается путем вычисления хеш-процента (table.length-1) ключа;
  2. В соответствии с полученной записью, если запись существует и ключ записи оказывается равным ThreadLocal, то объект записи возвращается напрямую;
  3. В противном случае, то есть соответствующий Entry не может быть найден в этой позиции, вызовите getEntryAfterMiss.

Копать так глубоко, приводит кgetEntryAfterMissметод:

getEntryAfterMiss

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

Мы должны рассмотреть этот метод в сочетании с предыдущим шагом, потому что предыдущий шаг не удовлетворяетe != null && e.get() == keyвплоть до вызоваgetEntryAfterMiss, поэтому сначала, если e равно нулю, затемgetEntryAfterMissИли вернуть null напрямую, если он не удовлетворенe.get() == key, затем войдите в цикл while, вот непрерывный цикл, если e не все время пусто, затем вызовите nextIndex, продолжайте увеличивать i и сделайте два суждения в этом процессе:

  1. еслиk==key, то представитель находит нужную Запись и сразу возвращается;
  2. еслиk==null, то доказывает, что ключ в этой записи уже нулевой, то эта запись является просроченным объектом, который вызывается здесьexpungeStaleEntryОчистите вход. Вот ответ на яму, оставленную спереди,То есть, когда ThreadLocal Ref уничтожается, экземпляр ThreadLocal будет подвергнут сборке мусора, потому что на него указывает только слабая ссылка в Entry, ключ Entry утерян, а значение может привести к утечке памяти., На самом деле, в каждой операции получения и установки запись с нулевым ключом будет постоянно очищаться.

Зачем зацикливать поиск?

Здесь вы можете сразу перейти к методу set ниже, в основном из-за метода обработки хеш-коллизий.Все мы знаем, что HashMap использует метод zipper для обработки хэш-коллизий, то есть, если в позиции уже есть элемент, связанный список используется для связывания конфликтующих элементов.За элементом ThreadLocal использует метод открытого адреса, то есть после конфликта вставляемый элемент помещается в место, где вставляемый элемент является нулевым после позиции Для получения подробной информации о разнице между двумя методами, пожалуйста, обратитесь к:Основной метод разрешения коллизий хэшей (HASH). Таким образом, приведенный выше цикл связан с тем, что не обязательно существует запись, ключ которой точно равен ключу, который мы хотим найти в позиции i, которую мы вычислили в первый раз, поэтому мы можем только продолжать цикл в конце, чтобы выяснить, является ли это вставлен сзади, пока не найдите элемент, который имеет значение null, потому что, если он вставлен, он также будет иметь значение null.

Проанализировав причины цикличности, можно на самом деле углубитьсяexpungeStaleEntryПосмотрите, как он очищается.

expungeStaleEntry

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

Приведенный выше код состоит из двух основных частей:

  1. expunge entry at staleSlot: Этот абзац в основном устанавливает значение записи в позиции i равным нулю, и ссылка на запись также устанавливается равной нулю, тогда система естественным образом очистит эту память во время GC;
  2. Rehash until we encounter null: Этот раздел представляет собой массив Entry перед нулем после сканирования позиции staleSlot и очищает каждую Entry, ключ которой равен null.В то же время, если ключ не пустой, сделайте перехеширование и скорректируйте его позицию.

Зачем перефразировать?

Поскольку в процессе очистки мы установим значение null, если область за этим значением связана с предыдущим, то при следующем поиске цикла будет найден только null.

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

установить метод

Также мы примерно описали идею метода set в циклическом поиске метода get, то есть метода открытого адреса, посмотрим на конкретный код ниже:

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

Первый — получить текущий поток и получить ThreadLocalMap в соответствии с потоком.Если есть ThreadLocalMap, вызовите map.set(ThreadLocal> key, Object value), если нет, вызовите createMap для его создания.

map.set

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

Посмотрите на код выше:

  1. Сначала вычисляем позицию i по ключу, а затем находим Вход в позиции i,
  2. Если Запись уже существует и ключ равен входящему ключу, то непосредственно присвойте Записи новое значение в это время.
  3. Если запись существует, но ключ имеет значение null, вызовите replaceStaleEntry, чтобы заменить запись, ключ которой пуст.
  4. Продолжайте зацикливать и обнаруживать, пока он не встретит нулевое место. В это время, если он не вернулся во время цикла, создайте новую запись в нулевой позиции, вставьте ее и увеличьте размер на 1.
  5. Наконец, вызовите cleanSomeSlots, эта функция не будет вдаваться в подробности, вам нужно только знать, что упомянутая выше функция expungeStaleEntry по-прежнему вызывается внутри для очистки записи, ключ которой равен нулю, и, наконец, возвращает, была ли запись очищена, а затем судитьsz>thresgold, Здесь нужно судить, достигнуто ли условие повторного хеширования, и если да, то будет вызвана функция повторного хеширования.

В приведенном выше коде есть две функции, которые необходимо проанализировать.

replaceStaleEntry

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

Прежде всего, мы помним, что предыдущий шаг — это вызов replaceStaleEntry, потому что ключ записи в этой позиции равен нулю.

  1. Первый цикл for: находим позицию, где ключ нулевой и записываем как slotToExpunge, это для последующего процесса очистки, так что можете не обращать внимания;
  2. Второй цикл for: мы начинаем с staleSlot до следующего нуля.Если мы находим запись, ключ которой равен входящему ключу, присваиваем этой записи новое значение и обмениваем ее на запись в позиции staleSlot, а затем вызываем CleanSomeSlots Очистить запись, ключ которой равен нулю.
  3. Если нет Записи, ключ которой равен входящему ключу, создайте новую Запись в staleSlot. Функция, наконец, снова очищает запись с пустым ключом.

законченныйreplaceStaleEntry, и еще одна важная функцияrehashИ условия перепрошивки:

прежде всегоsz > thresholdкогда звонятrehash:

rehash

private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

После очистки Ввод пустого ключа, если размер больше3/4порог, затем позвонитеresizeфункция:

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

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

remove

Последнее, что нужно изучить, — это функция удаления, которая используется для удаления неиспользуемой записи с карты. Он также сначала вычисляет хеш-значение. Если он не сработает в первый раз, он будет зацикливаться до нуля. Во время этого процесса также будет вызываться expungeStaleEntry для очистки пустого узла ключа. код показывает, как показано ниже:

private void remove(ThreadLocal<?> key) {
    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)]) {
         if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

Лучшие практики использования ThreadLocal

Мы обнаружили, что независимо от того, является ли это методом set, get или remove, запись с нулевым ключом будет стерта во время процесса, поэтому значение в записи не будет иметь строгой цепочки ссылок и будет повторно использовано во время GC. Так как же может быть утечка памяти? Но приведенная выше идея состоит в том, чтобы предположить, что вы вызываете метод get или set, который мы не вызывали много раз, поэтому наилучшей практикой является *

1. Пользователю необходимо вручную вызвать функцию удаления, чтобы удалить ThreadLocal, который больше не используется.

2. Также попробуйте установить для ThreadLocal значениеprivate static, так что ThreadLocal попытается умереть вместе с самим потоком.