Углубленная интерпретация ThreadLocal

Java

Публичный аккаунт WeChat:I am CR7
Если у вас есть какие-либо вопросы или предложения, пожалуйста, оставьте сообщение ниже;
Последнее обновление: 2019-01-12

предисловие

Вслед за предыдущей статьей»Статьи об обработке запросов на анализ исходного кода Spring Cloud Netflix Zuul«Один из двух артефактов, используемых RequestContext, упомянутых в «ThreadLocal», в этой статье проводится углубленный анализ, чтобы устранить барьер знаний для всех.

Hello World

Прежде чем мы начнем наш углубленный анализ, давайте взглянем на официальный пример:

Источник взят из комментариев к классу ThreadLocal, где основной метод добавлен автором.

 1import java.util.concurrent.atomic.AtomicInteger;
2
3public class ThreadId {
4    // Atomic integer containing the next thread ID to be assigned
5    private static final AtomicInteger nextId = new AtomicInteger(0);
6
7    // Thread local variable containing each thread's ID
8    private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
9        @Override
10        protected Integer initialValue() {
11            return nextId.getAndIncrement();
12        }
13    };
14
15    // Returns the current thread's unique ID, assigning it if necessary
16    public static int get() {
17        return threadId.get();
18    }
19
20    public static void main(String[] args) {
21        for (int i = 0; i < 5; i++) {
22            new Thread(new Runnable() {
23                @Override
24                public void run() {
25                    System.out.println("threadName=" + Thread.currentThread().getName() + ",threadId=" + ThreadId.get());
26                }
27            }).start();
28        }
29    }
30}

Результаты приведены ниже:

1threadName=Thread-0,threadId=0
2threadName=Thread-1,threadId=1
3threadName=Thread-2,threadId=2
4threadName=Thread-3,threadId=3
5threadName=Thread-4,threadId=4

я спрашиваю: Прочитав этот пример, вы знаете, что делает ThreadLocal?
Вы отвечаете: Не знаю, не чувствую, пример hello world вообще не вызывает интереса.
ты спрашиваешь: Кто, осмелитесь привести пример производственного уровня, который реально можно использовать в работе?
Я отвечаю: Да, ты "Хозяин", я сделаю все, что ты скажешь. все еще помню"Статьи об обработке запросов на анализ исходного кода Spring Cloud Netflix Zuul"RequestContext упоминается в? Это приложение производственного уровня. Каков основной принцип Zuul? Это поместить запрос в цепочку фильтров и обрабатывать его один за другим. Между фильтрами нет прямой вызывающей связи. Результаты обработки сохраняются в RequestContext и передаются, и этот RequestContext является объектом типа ThreadLocal! ! !

 1public class RequestContext extends ConcurrentHashMap<String, Object> {
2
3    protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
4        @Override
5        protected RequestContext initialValue() {
6            try {
7                return contextClass.newInstance();
8            } catch (Throwable e) {
9                throw new RuntimeException(e);
10            }
11        }
12    };
13
14    public static RequestContext getCurrentContext() {
15        if (testContext != null) return testContext;
16
17        RequestContext context = threadLocal.get();
18        return context;
19    }
20}

В качестве примера возьмем предварительный фильтр DebugFilter в Zuul:

 1public class DebugFilter extends ZuulFilter {
2
3    @Override
4    public Object run() {
5        // 获取ThreadLocal对象RequestContext
6        RequestContext ctx = RequestContext.getCurrentContext();
7        // 它是一个map,可以放入数据,给后面的过滤器使用
8        ctx.setDebugRouting(true);
9        ctx.setDebugRequest(true);
10        return null;
11    }
12
13}

ты спрашиваешь: После долгого разговора, что это такое и в чем его польза?Можете ли вы дать мне представление?
Я отвечаю:могу! Должен уметь! ! !

What is this

Что это такое? Это класс Java, который поддерживает дженерики.Помимо статического внутреннего класса ThreadLocalMap внутри, он на самом деле не имеет нескольких строк кода.Если вы не верите в это, вы можете убедиться в этом сами. Для чего его используют? Аннотация к классу очень понятна:

  • Это позволяет потокам иметь свои собственные внутренние эксклюзивные переменные.
  • Каждый поток может работать через методы get и set.
  • Вы можете переопределить метод initialValue, чтобы указать исключительное для потока значение.
  • Обычно он используется для изменения частных статических конечных свойств в классе, чтобы установить некоторую информацию о состоянии для потока, такую ​​как идентификатор пользователя или идентификатор транзакции.
  • Каждый поток имеет слабую ссылку на экземпляр threadLocal, который не будет очищен сборщиком мусора, пока поток жив или к экземпляру threadLocal можно получить доступ.

Если вы любите задавать вопросы, у вас должны быть сомнения.Демонстрация вызывает только метод ThreadLocal.get().Как он достигает всего этого величия? Об этом автор и расскажет дальше, поехали~~~

у меня есть карта

Без лишних слов давайте посмотрим на внутреннюю реализацию метода get:

получить() исходный код
 1public T get() {
2    Thread t = Thread.currentThread();
3    ThreadLocalMap map = getMap(t);
4    if (map != null) {
5        ThreadLocalMap.Entry e = map.getEntry(this);
6        if (e != null) {
7            @SuppressWarnings("unchecked")
8            T result = (T)e.value;
9            return result;
10        }
11    }
12    return setInitialValue();
13}

Логика проста:

  • Получить ThreadLocalMap внутри текущего потока
  • Если карта существует, получить значение, соответствующее текущему ThreadLocal
  • Карта не существует или значение не может быть найдено, затем вызовите setInitialValue для инициализации
исходный код setInitialValue()
 1private T setInitialValue() {
2    T value = initialValue();
3    Thread t = Thread.currentThread();
4    ThreadLocalMap map = getMap(t);
5    if (map != null)
6        map.set(this, value);
7    else
8        createMap(t, value);
9    return value;
10}

Логика тоже очень проста:

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

Для облегчения понимания автор специально нарисовал временную диаграмму, смотрите:

get方法时序图
получить диаграмму последовательности методов
резюме

А пока можете ответить по принципу реализации ThreadLocal? Да, карта, карта под названием ThreadLocalMap, это ключ. Каждый поток имеет закрытую переменную типа ThreadLocalMap. При добавлении объекта ThreadLocal в поток он сохраняется в этой карте, поэтому потоки не будут мешать друг другу. Подводя итог, в одном предложении: у меня есть мой молодой, о нет, у меня есть моя карта. Выяснив это, вы станете намного увереннее использовать его. Однако означает ли это, что его можно смело использовать? На самом деле, не обязательно, вас ждет «большая яма».

магия удалить

«Большая яма» относится к проблеме утечек памяти, вызванных неправильным использованием ThreadLocal. Автор приводит два примера кода, чтобы проиллюстрировать эту проблему.

Источник кода взят из Stack Overflow:stackoverflow.com/questions/1…

Пример первый:
 1public class MemoryLeak {
2
3    public static void main(String[] args) {
4        new Thread(new Runnable() {
5            @Override
6            public void run() {
7                for (int i = 0; i < 1000; i++) {
8                    TestClass t = new TestClass(i);
9                    t.printId();
10                    t = null;
11                }
12            }
13        }).start();
14    }
15
16    static class TestClass{
17        private int id;
18        private int[] arr;
19        private ThreadLocal<TestClass> threadLocal;
20        TestClass(int id){
21            this.id = id;
22            arr = new int[1000000];
23            threadLocal = new ThreadLocal<>();
24            threadLocal.set(this);
25        }
26
27        public void printId(){
28            System.out.println(threadLocal.get().id);
29        }
30    }
31}

результат операции:

 10
21
32
43
5...省略...
6440
7441
8442
9443
10444
11Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
12    at com.gentlemanqc.MemoryLeak$TestClass.<init>(MemoryLeak.java:33)
13    at com.gentlemanqc.MemoryLeak$1.run(MemoryLeak.java:16)
14    at java.lang.Thread.run(Thread.java:745)

Небольшая модификация приведенного выше кода, см.:

 1public class MemoryLeak {
2
3    public static void main(String[] args) {
4        new Thread(new Runnable() {
5            @Override
6            public void run() {
7                for (int i = 0; i < 1000; i++) {
8                    TestClass t = new TestClass(i);
9                    t.printId();
10                    t.threadLocal.remove();
11                }
12            }
13        }).start();
14    }
15
16    static class TestClass{
17        private int id;
18        private int[] arr;
19        private ThreadLocal<TestClass> threadLocal;
20        TestClass(int id){
21            this.id = id;
22            arr = new int[1000000];
23            threadLocal = new ThreadLocal<>();
24            threadLocal.set(this);
25        }
26
27        public void printId(){
28            System.out.println(threadLocal.get().id);
29        }
30    }
31}

результат операции:

10
21
32
43
5...省略...
6996
7997
8998
9999

Утечка памяти, нормальное завершение, единственное отличие в коде сравнения: t = null заменено на t.threadLocal.remove(), Ого, волшебное удаление! ! ! Автор сначала оставляет интригу, а причины пока не анализирует. Давайте сначала рассмотрим два метода, используемых в приведенном выше примере: set() и remove().

установить (значение T) исходный код
1public void set(T value) {
2    Thread t = Thread.currentThread();
3    ThreadLocalMap map = getMap(t);
4    if (map != null)
5        map.set(this, value);
6    else
7        createMap(t, value);
8}

Логика проста:

  • Получить ThreadLocalMap внутри текущего потока
  • Если карта существует, добавьте текущий ThreadLocal и значение на карту.
  • Если карта не существует, создайте ThreadLocalMap и сохраните ее в текущем потоке.
удалить исходный код
1public void remove() {
2    ThreadLocalMap m = getMap(Thread.currentThread());
3    if (m != null)
4     m.remove(this);
5}

Одним словом, получить ThreadLocalMap внутри текущего потока и удалить объект ThreadLocal с карты, если он существует.

резюме

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

Вездесущий ThreadLocalMap

Или старые правила, давайте сначала посмотрим на аннотации к классу, перевод такой:

  • ThreadLocalMap — это пользовательская хэш-карта, предназначенная для сохранения локальных переменных потоков.
  • Его операции ограничены классом ThreadLocal и не доступны внешнему миру.
  • Этот класс используется для частных переменных threadLocals и inheritableThreadLocals класса Thread.
  • Чтобы сохранить большое количество экземпляров threadLocal с длительным временем выживания, записи хеш-таблицы используют WeakReferences в качестве типа ключа.
  • Как только в хеш-таблице закончится место, запись с нулевым ключом будет очищена.

Давайте посмотрим на информацию объявления класса:

 1static class ThreadLocalMap {
2
3    // hash map中的entry继承自弱引用WeakReference,指向threadLocal对象
4    // 对于key为null的entry,说明不再需要访问,会从table表中清理掉
5    // 这种entry被成为“stale entries”
6    static class Entry extends WeakReference<ThreadLocal<?>> {
7        /** The value associated with this ThreadLocal. */
8        Object value;
9
10        Entry(ThreadLocal<?> k, Object v) {
11            super(k);
12            value = v;
13        }
14    }
15
16    /**
17     * The initial capacity -- MUST be a power of two.
18     */
19    private static final int INITIAL_CAPACITY = 16;
20
21    /**
22     * The table, resized as necessary.
23     * table.length MUST always be a power of two.
24     */
25    private Entry[] table;
26
27    /**
28     * The number of entries in the table.
29     */
30    private int size = 0;
31
32    /**
33     * The next size value at which to resize.
34     */
35    private int threshold; // Default to 0
36
37    /**
38     * Set the resize threshold to maintain at worst a 2/3 load factor.
39     */
40    private void setThreshold(int len) {
41        threshold = len * 2 / 3;
42    }
43
44    /**
45     * Construct a new map initially containing (firstKey, firstValue).
46     * ThreadLocalMaps are constructed lazily, so we only create
47     * one when we have at least one entry to put in it.
48     */
49    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
50        table = new Entry[INITIAL_CAPACITY];
51        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
52        table[i] = new Entry(firstKey, firstValue);
53        size = 1;
54        setThreshold(INITIAL_CAPACITY);
55    }
56}

При создании ThreadLocalMap массив типа Entry фактически строится внутри, начальный размер — 16, пороговое значение — 2/3 длины массива, тип Entry — WeakReference, и имеется слабая ссылка на объект ThreadLocal.

Почему Entry принимает тип WeakReference?

В сборке мусора Java, чтобы увидеть, нужно ли перерабатывать объект или нет, нужно увидеть, доступен ли объект. Что достижимо, так это то, можно ли получить доступ к объекту по ссылке. (Конечно, стратегия сборки мусора намного сложнее. Для простоты понимания я кратко объясню ее здесь.)

После jdk1.2 ссылки делятся на четыре типа: сильные ссылки, слабые ссылки, мягкие ссылки и виртуальные ссылки. Строгая ссылка — это наша обычно используемая Object obj = new Object(), а obj — это сильная ссылка, указывающая на пространство памяти объекта. Когда места в памяти недостаточно, сборщик мусора Java обнаруживает, что объект имеет сильную ссылку, и скорее выдает ошибку OutofMemory, чем освобождает пространство памяти, на которое ссылаются строго. Слабая ссылка, или WeakReference, означает, что когда объект имеет только слабые ссылки, указывающие на него, сборщик мусора утилизирует его независимо от того, достаточно ли текущей памяти. И наоборот, будет ли этот объект собираться мусором, зависит от того, есть ли на него сильные ссылки. ThreadLocalMap делает это, потому что не хочет влиять на свою сборку мусора, потому что хранит сам объект ThreadLocal, но отдает инициативу вызывающей стороне. использоваться.

проблема с переполнением памяти

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

内存状态
состояние памяти
  • t — это ссылка, возвращаемая созданием объекта TestClass, временной переменной, которая извлекается после цикла for.
  • thread — это ссылка, возвращаемая при создании объекта Thread.Во время выполнения метода run стек временно выполняться не будет.

После вызова t=null, хотя адрес памяти MemoryLeak больше не может быть доступен через t

 1不能识别此Latex公式:
2TestClass@538,但是当前线程依旧存活,可以通过thread指向的内存地址,访问到Thread对象,从而访问到ThreadLocalMap对象,访问到value指向的内存空间,访问到arr指向的内存空间,从而导致Java垃圾回收并不会回收int[1000000]@541这一片空间。那么随着循环多次之后,不被回收的堆空间越来越大,最后抛出java.lang.OutOfMemoryError: Java heap space。
3
4您问:那为什么调用t.threadLocal.remove()就可以呢?
5
6我答:这就得看remove方法里究竟做了什么了,请看:
7
image

8是不是恍然大悟?来看下调用remove方法之后的内存状态:
9
image

10因为remove方法将referent和value都被设置为null,所以ThreadLocal@540和Memory
Адреса памяти, соответствующие TestClass@538, становятся недоступными, и сборщик мусора Java естественным образом освобождает эту память, так что не возникает ошибки утечки памяти.

резюме

Отвечая на "Статьи об обработке запросов на анализ исходного кода Spring Cloud Netflix Zuul", есть очень важный класс: ZuulServlet, который является типичным случаем применения ThreadLocal в практических сценариях. Пожалуйста, посмотри:

 1public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
2    try {
3        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
4        RequestContext context = RequestContext.getCurrentContext();
5        context.setZuulEngineRan();
6
7        try {
8            preRoute();
9        } catch (ZuulException e) {
10            error(e);
11            postRoute();
12            return;
13        }
14        try {
15            route();
16        } catch (ZuulException e) {
17            error(e);
18            postRoute();
19            return;
20        }
21        try {
22            postRoute();
23        } catch (ZuulException e) {
24            error(e);
25            return;
26        }
27
28    } catch (Throwable e) {
29        error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
30    } finally {
31        RequestContext.getCurrentContext().unset();
32    }
33}

Вы обнаружили, что после обработки HTTP-запроса предварительным фильтром, фильтром маршрутизации и постфильтром будет вызван метод, да, наконец, RequestContext.getCurrentContext().unset(). Зайдите в RequestContext и посмотрите:

1public void unset() {
2    threadLocal.remove();
3}

Видя нет, снова появилось удаление артефакта. Кстати говоря, получается ли у вас, чтобы ThreadLocal правильно использовал «позу»?

Дополнительные возможности ThreadLocalMap

Автор уже писал статьи о TreeMap и HashMap, все реализации Map имеют свои собственные методы уменьшения и разрешения хеш-конфликтов. Как здесь обрабатывается ThreadLocalMap? Пожалуйста, посмотрите вниз.

Как уменьшить коллизии хэшей

Просмотрите исходный код для добавления элементов в ThreadLocalMap:

  • Метод 1: Метод построения
1ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
2    table = new Entry[INITIAL_CAPACITY];
3    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
4    table[i] = new Entry(firstKey, firstValue);
5    size = 1;
6    setThreshold(INITIAL_CAPACITY);
7}
  • Способ 2: установить метод
 1private void set(ThreadLocal<?> key, Object value) {
2
3    Entry[] tab = table;
4    int len = tab.length;
5    int i = key.threadLocalHashCode & (len-1);
6
7    for (Entry e = tab[i];
8         e != null;
9         e = tab[i = nextIndex(i, len)]) {
10        ThreadLocal<?> k = e.get();
11
12        if (k == key) {
13            e.value = value;
14            return;
15        }
16
17        if (k == null) {
18            replaceStaleEntry(key, value, i);
19            return;
20        }
21    }
22
23    tab[i] = new Entry(key, value);
24    int sz = ++size;
25    if (!cleanSomeSlots(i, sz) && sz >= threshold)
26        rehash();
27}

Где i — это индекс, сохраненный ThreadLocal в ThreadLocalMap, который рассчитывается как: key.threadLocalHashCode & (len-1). Давайте сначала посмотрим, что такое threadLocalHashCode?

1private final int threadLocalHashCode = nextHashCode();

Другими словами, каждый ThreadLocal будет генерировать значение int в соответствии с nextHashCode в качестве хэш-значения, а затем получать младшие N бит хэш-значения в соответствии с этим хеш-значением & (длина массива-1) (с len как 16, 16 -1 гарантирует, что все младшие четыре бита равны 1, чтобы получить младшие четыре бита самого хеш-значения), тем самым получая позицию индекса в массиве. Итак, как это уменьшает коллизии хэшей? Секрет заключается в этом методе nextHashCode.

1private static AtomicInteger nextHashCode = new AtomicInteger();
2
3private static final int HASH_INCREMENT = 0x61c88647;
4
5private static int nextHashCode() {
6    return nextHashCode.getAndAdd(HASH_INCREMENT);
7}

Что такое 0x61c88647? Преобразование в десятичное число равно 1640531527. Преобразование 2654435769 в тип int равно -1640531527. 2654435769 равно (квадратный корень 5-1)/2, умноженный на 2 в 32-й степени. Что такое (корень 5-1)/2? это золотое сечение, которое составляет примерно 0,618. То есть 0x61c88647 понимается как золотое сечение, умноженное на 2 в 32-й степени. какая польза? Он может волшебным образом гарантировать, что хеш-значение, сгенерированное nextHashCode, будет равномерно распределено в степени 2 и менее 2 в степени 32. Давайте посмотрим пример:

 1public class ThreadLocalHashCodeTest {
2
3    private static AtomicInteger nextHashCode =
4            new AtomicInteger();
5
6    private static final int HASH_INCREMENT = 0x61c88647;
7
8    private static int nextHashCode() {
9        return nextHashCode.getAndAdd(HASH_INCREMENT);
10    }
11
12    public static void main(String[] args){
13        for (int i = 0; i < 16; i++) {
14            System.out.print(nextHashCode() & 15);
15            System.out.print(" ");
16        }
17        System.out.println();
18        for (int i = 0; i < 32; i++) {
19            System.out.print(nextHashCode() & 31);
20            System.out.print(" ");
21        }
22        System.out.println();
23        for (int i = 0; i < 64; i++) {
24            System.out.print(nextHashCode() & 63);
25            System.out.print(" ");
26        }
27    }
28}

Выходной результат:

10 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 
216 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 
316 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 

Смотрите нет, значения индекса элемента отлично хешируются в массиве, и коллизий нет.

Как разрешить коллизии хешей

ThreadLocalMap использует метод золотого сечения, что значительно снижает вероятность коллизии хэшей, но такая ситуация все же существует, и если она возникает, то как ее решить? Пожалуйста, посмотри:

 1private void set(ThreadLocal<?> key, Object value) {
2
3    Entry[] tab = table;
4    int len = tab.length;
5    int i = key.threadLocalHashCode & (len-1);
6
7    // 出现哈希冲突
8    for (Entry e = tab[i];
9         e != null;
10         e = tab[i = nextIndex(i, len)]) {
11        ThreadLocal<?> k = e.get();
12
13        // 如果是同一个对象,则覆盖value值
14        if (k == key) {
15            e.value = value;
16            return;
17        }
18
19        // 如果key为null,则替换它的位置
20        if (k == null) {
21            replaceStaleEntry(key, value, i);
22            return;
23        }
24
25        // 否则往后一个位置找,直到找到空的位置
26    }
27
28    tab[i] = new Entry(key, value);
29    int sz = ++size;
30    if (!cleanSomeSlots(i, sz) && sz >= threshold)
31        rehash();
32}

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

1private static int nextIndex(int i, int len) {
2    return ((i + 1 < len) ? i + 1 : 0);
3}
Расширение

Из кода в методе set мы знаем, что есть две предпосылки для расширения ThreadLocalMap:

  • !cleanSomeSlots(i, sz)
  • size >= threshold

Количество элементов больше, чем порог для расширения, что легко понять, так что же означает еще одна посылка? Давайте посмотрим, что делает cleanSomeSlots():

 1private boolean cleanSomeSlots(int i, int n) {
2    boolean removed = false;
3    Entry[] tab = table;
4    int len = tab.length;
5    do {
6        i = nextIndex(i, len);
7        Entry e = tab[i];
8        if (e != null && e.get() == null) {
9            n = len;
10            removed = true;
11            i = expungeStaleEntry(i);
12        }
13    } while ( (n >>>= 1) != 0);
14    return removed;
15}

Аннотация к методу очень ясна.Исходя из текущей позиции элемента, просмотрите элементы в массиве, чтобы определить, является ли это «устаревшей записью». Как упоминалось ранее при объявлении информации класса ThreadLocalMap, «устаревшая запись» относится к тем записям, ключ которых равен null. Метод cleanSomeSlots должен найти их и вызвать метод expangeStaleEntry для очистки. Возвращает true, если найдено, иначе false.
ты спрашиваешь: Почему расширение зависит от возвращаемого значения?
Я отвечаю: Поскольку после обнаружения вызывается метод expungeStaleEntry для очистки.

 1private int expungeStaleEntry(int staleSlot) {
2            Entry[] tab = table;
3            int len = tab.length;
4
5    // expunge entry at staleSlot
6    tab[staleSlot].value = null;
7    tab[staleSlot] = null;
8    size--;
9
10    // 省略
11}

Если вы видите «нет», размер будет уменьшен на единицу, а добавление элемента приведет к увеличению размера на 1. После обнаружения cleanSomeSlots один или несколько элементов будут очищены.Минимальный вычитаемый размер равен 1, поэтому, если он возвращает true, нет необходимости судить, больше ли размер порогового значения или равен ему.
Что ж, как только предварительные условия соблюдены, вызывается метод rehash, а емкость в это время не расширялась:

1private void rehash() {
2    // 先清理stale entry,会导致size变化
3    expungeStaleEntries();
4
5    // 如果size大于等于3/4阈值,则扩容
6    if (size >= threshold - threshold / 4)
7        resize();
8}

Ха-ха, вот настоящее расширение, которое нужно расширить:

  1. Позиция текущего вставленного элемента, нет устаревшей записи, которую необходимо очистить в будущем.
  2. размер больше или равен пороговому значению
  3. После очистки устаревшей записи размер больше или равен пороговому значению 3/4.

Теперь, когда условия ясны, как расширить мощность после того, как они будут выполнены?

 1private void resize() {
2    Entry[] oldTab = table;
3    int oldLen = oldTab.length;
4    int newLen = oldLen * 2;
5    // 新建一个数组,按照2倍长度扩容
6    Entry[] newTab = new Entry[newLen];
7    int count = 0;
8
9    for (int j = 0; j < oldLen; ++j) {
10        Entry e = oldTab[j];
11        if (e != null) {
12            ThreadLocal<?> k = e.get();
13            if (k == null) {
14                e.value = null; // Help the GC
15            } else {
16                // key不为null,重新计算索引位置
17                int h = k.threadLocalHashCode & (newLen - 1);
18                while (newTab[h] != null)
19                    h = nextIndex(h, newLen);
20                // 插入新的数组中索引位置
21                newTab[h] = e;
22                count++;
23            }
24        }
25    }
26
27    // 阈值为长度的2/3
28    setThreshold(newLen);
29    size = count;
30    table = newTab;
31}

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

Пополнить

Интересно, заметили ли вы, что когда в ThreadLocalMap происходит коллизия хэшей, он линейно проверяет, пока не найдет пустое место. Такого рода эффективность очень низка, так почему же боги Java делают это, когда пишут код? Автор считает, что в зависимости от используемого хеш-алгоритма именно благодаря nextHashCode() вероятность коллизий гарантированно будет низкой. Кроме того, ThreadLocalMap уделяет большое внимание очистке «устаревшей записи» в процессе обработки и вовремя освобождает свободную позицию, тем самым снижая неэффективность, вызванную линейным обнаружением.

Суммировать

В этой статье сказано так много, в основном для того, чтобы все поняли, как правильно использовать ThreadLocal и принцип его использования. Следующие экстры чисто интересны, можно сравнить с предыдущей авторской"Вставка элемента HashMap«Содержание внутри, дивергентное мышление. Автор знает, что уровень ограничен, если у вас есть какие-либо замечания или предложения, пожалуйста, оставьте сообщение и укажите, я очень благодарен! ! ! Наконец, спасибо за вашу постоянную поддержку, Чжу Цзинань, Ци Чен, 12 января 2019 г.