Интервьюер: Расскажите, что вы знаете о ThreadLocal?

Java

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

Ява прошлаThreadLocalдля реализации локального хранилища потоков.

Эта идея тоже очень понятна, то есть у каждого потока должна быть своя локальная переменная, тогда сделайте приватное свойство в Thread.ThreadLocal.ThreadLocalMap threadLocals = null;Это отношение показано на рисунке ниже

ThreadLocal

Простое приложение выглядит следующим образом

    public class Demo {
		private static final ThreadLocal<Foo> fooLocal = new ThreadLocal<Foo>();
 
		public static Foo getFoo() {
			return fooLocal.get();
		}
 
		public static void setFoo(Foo foo) {
			fooLocal.set(foo);
		}
	}

Присмотритесь к тому, что происходит внутри,ThreadLocalMapдаThreadLocalвнутренний статический класс, который хотя и называетсяMapНо иjava.util.MapЭто не связано, просто он реализует такие функции, какMap

    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;

можно увидетьThreadLocalMapEстьEntryМассивы, только массивы не имеют ничего подобногоHashMapТаким образом, существует связанный список, поэтому, когда хэши сталкиваются,ThreadLocalMapдаИспользуйте линейное обнаружение для разрешения конфликтов хэшей.

Линейное обнаружение сначала основано на начальномkeyизhashcodeЗначение определяет, что элемент находится вtableпозиция в массиве, если уже есть другаяkeyЕсли элемент значения занят, используется фиксированный алгоритм для поиска следующей позиции с определенным размером шага, пока не будет найдена позиция, которую можно сохранить. существуетThreadLocalMapРазмер шага равен 1.

Разрешать хеш-конфликты таким образом очень неэффективно, поэтому обратите внимание на количество ThreadLocals.

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

и вы можете видеть этоEntryПучокThreadLocalВ качестве ключа используется слабая ссылка. Так зачем создавать слабую ссылку (пока существует объект слабой ссылки GC, он будет переработан)?

во-первыхThreadLocalНикакое значение не хранится внутри, его роль только тогда, когда нашThreadLocalMap的key, чтобы поток мог получить соответствующийvalue. Когда нам не нужно использовать этот ключ, мы ставимfooLocal=nullТаким образом, сильные ссылки исчезли. Предполагая, что в Записи также есть сильная ссылка, она равна этомуThreadLocalПо-прежнему существует сильная ссылка на экземпляр, поэтому мы хотим, чтобы сборщик мусора переработалfooLocalЕго нельзя переработать. Тогда некоторые люди могут подумать, что вам не очень опасно делать слабые ссылки, на случай, если GC исчезнет? Не бойся, покаfooLocalЭта сильная ссылка находится в этомThreadLocalЭкземпляры не будут переработаны. (Сильные и слабые ссылки см. в моей предыдущей статьеРазница между четырьмя методами цитирования)

Таким образом, слабые ссылки в основном делают бесполезнымиThreadLocalОчищено ГК.

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

Но java это учитывает. при звонкеget()、set()Метод найдет запись, ключ которой был убит, и уничтожит ее. и обеспечиваетremove()метод. Несмотря на то чтоget()、set()будет убиратьkeyзаnull的Entry, но очищается не при каждом вызове, а только приgetКогда хэш пропущен напрямую илиsetИногда хэш попадает не напрямую, когда линейное обнаружение запущено, оно будет очищено, когда ключ будет нулевым.

        //get 方法
        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); //直接没命中
        }
        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)  //探测到key是null的就清理
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len); //否则继续
                e = tab[i];
            }
            return null;
        }
        //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;    //如果已经有就替换原有的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();
        }

Поэтому, когда нет необходимостиthreadlocalОн по-прежнему показывает вызов, когдаremove()метод лучше.

Эпилог

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


Если есть ошибки, поправьте меня!

Личный публичный аккаунт: стратегия прокачки да

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