Параллелизм JAVA — самостоятельный вопрос и самостоятельный ответ для изучения ThreadLocal

Java задняя часть Безопасность дизайн

предисловие

ThreadLocalМногие студенты не понимают, что это такое и для чего его можно использовать. Но его часто спрашивают на собеседованиях, так что в этот раз я буду учиться вместе с вами.ThreadLocalэтот класс.

Ниже я узнаю наши в виде вопросов интервью-ThreadLocalКласс (анализ исходного кода основан на JDK8)

Эта статья одновременно публикуется в Цзяньшу:у-у-у. Краткое описание.com/afraid/807686414…

Вопросы и ответы

1.

просить:ThreadLocalпонял? Можете ли вы рассказать мне о его основном использовании?

отвечать:

  • Из официальной пары JAVAThreadLocalИллюстративное определение класса (определено в примере кода):ThreadLocalКлассы используются для предоставления локальных переменных в потоке. Доступ к таким переменным осуществляется в многопоточной среде (черезgetиsetдоступ к методу), чтобы гарантировать, что переменные каждого потока относительно независимы от переменных в других потоках.ThreadLocalПримеры обычноprivate staticТипы, используемые для связывания потоков и контекстов потоков.

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

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

Образец кода:


/**
 * 该类提供了线程局部 (thread-local) 变量。 这些变量不同于它们的普通对应物,
 * 因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量
 * 它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段
 * 它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
 *
 * 例如,以下类生成对每个线程唯一的局部标识符。
 * 
 * 线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,
 * 在后续调用中不会更改。
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // 原子性整数,包含下一个分配的线程Thread ID 
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // 每一个线程对应的Thread ID
 *     private static final ThreadLocal<Integer> threadId =
 *         new ThreadLocal<Integer>() {
 *             @Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // 返回当前线程对应的唯一Thread ID, 必要时会进行分配
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * 每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的
 * 在线程消失之后,其线程局部实例的所有副本都会被垃圾回收,(除非存在对这些副本的其他引用)。
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */
public class ThreadLocal<T> {
·····
   /**
     * 自定义哈希码(仅在ThreadLocalMaps中有用)
     * 可用于降低hash冲突
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * 生成下一个哈希码hashCode. 生成操作是原子性的. 从0开始
     * 
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();


    /**
     * 表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量 
     */
    private static final int HASH_INCREMENT = 0x61c88647;


    /**
     * 返回下一个哈希码hashCode
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
·····

}
  • вnextHashCode()Метод заключается в том, что атомарный класс продолжает добавлять 0x61c88647, что является очень особым числом, называемым хешированием Фибоначчи, Фибоначчи также имеет имя, называемое золотым сечением, что означает, что это увеличение числа в качестве значения хеш-функции сделает распределение хеш-таблицы более даже.

2.

просить:ThreadLocalКаков принцип реализации и как он гарантирует, что разные потоки локальных переменных не будут мешать друг другу?

отвечать:

  • Обычно, если я не смотрю на исходный код, я думаюThreadLocalустроено так: каждыйThreadLocalкласс создаетMap, а затем используйте идентификатор потокаthreadIDв видеMapизkey, локальная переменная, которая будет храниться какMapизvalue, чтобы можно было достичь эффекта изоляции значений каждого потока. Это самый простой метод проектирования, самый ранний JDK.ThreadLocalВот как это было разработано.

  • Однако схема проектирования была оптимизирована для JDK, и текущий JDK8ThreadLocalДизайн: каждыйThreadподдерживать одинThreadLocalMapхеш-таблица, хэш-таблицаkeyдаThreadLocalсам экземпляр,valueфактическое значение, которое нужно сохранитьObject.

  • Этот дизайн прямо противоположен тому, о чем мы говорили в начале, он имеет следующие преимущества:

    1) После этого дизайна каждыйMapхранитсяEntryсумма будет меньше, потому что предыдущая сумма хранения определяетсяThreadопределяется количествомThreadLocalколичество определяется.

    2) КогдаThreadПосле разрушения соответствующиеThreadLocalMapОн также будет уничтожен, что может уменьшить использование памяти.

ThreadLocal引用关系图- 图片来自于《简书 - 对ThreadLocal实现原理的一点思考》
Справочная диаграмма ThreadLocal — изображение взято из «Короткой книги — небольшое размышление о принципе реализации ThreadLocal».

Приведенное выше объяснение в основном относится к:Разница между ThreadLocal и синхронизированным?

3.

В: Можете ли вы сказать мнеThreadLocalЯвляется ли это основополагающим принципом реализации общих операций? как хранилищеset(T value),Получатьget(),Удалитьremove()и так далее.

отвечать:

  • перечислитьget()Операция получитьThreadLocalПри сохранении значения, сохраненного в соответствующем текущем потоке, выполняются следующие операции:

    1) Получить текущий потокThreadобъект, а затем получить объект потока, поддерживаемый в этом потокеThreadLocalMapобъект.

    2) Судить о текущемThreadLocalMapсуществует ли:

  • Если он существует, используйте текущийThreadLocalзаkey,перечислитьThreadLocalMapсерединаgetEntryМетод получает соответствующий объект хранения e. Найдите соответствующий объект хранения e и получите соответствующий объект хранения evalueзначение, которое является текущим потоком, который мы хотим связать с этимThreadLocalзначение, вернуть значение результата.
  • Если он не существует, это доказывает, что этот поток не поддерживаетсяThreadLocalMapобъект, звонокsetInitialValueметод для инициализации. возвращениеsetInitialValueинициализированное значение.

  • setInitialValueДействие метода заключается в следующем:

    1) позвонитьinitialValueПолучите инициализированное значение.

    2) Получить текущий потокThreadобъект, а затем получить объект потока, поддерживаемый в этом потокеThreadLocalMapобъект.

    3) Судить о текущемThreadLocalMapсуществует ли:

  • Если есть, звонитеmap.setустановить этот объектentry.

  • Если его нет, звонитеcreateMapпровестиThreadLocalMapинициализация объекта, и эта сущностьentryкак первое значение, сохраненное вThreadLocalMapсередина.

ПС: оThreadLocalMapСоответствующие связанные операции подробно описаны в следующем вопросе.

Образец кода:

    /**
     * 返回当前线程对应的ThreadLocal的初始值
     * 此方法的第一次调用发生在,当线程通过{@link #get}方法访问此线程的ThreadLocal值时
     * 除非线程先调用了 {@link #set}方法,在这种情况下,
     * {@code initialValue} 才不会被这个线程调用。
     * 通常情况下,每个线程最多调用一次这个方法,
     * 但也可能再次调用,发生在调用{@link #remove}方法后,
     * 紧接着调用{@link #get}方法。
     *
     * <p>这个方法仅仅简单的返回null {@code null};
     * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
     * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
     * 通常, 可以通过匿名内部类的方式实现
     *
     * @return 当前ThreadLocal的初始值
     */
    protected T initialValue() {
        return null;
    }

    /**
     * 创建一个ThreadLocal
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

    /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     *
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 找到对应的存储实体 e 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        // 如果map不存在,则证明此线程没有维护的ThreadLocalMap对象
        // 调用setInitialValue进行初始化
        return setInitialValue();
    }

    /**
     * set的变样实现,用于初始化值initialValue,
     * 用于代替防止用户重写set()方法
     *
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

    /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  • перечислитьset(T value)Когда операция устанавливает значение, которое будет сохранено в ThreadLocal, соответствующем текущему потоку, выполняются следующие операции:

    1) Получить текущий потокThreadобъект, а затем получить объект потока, поддерживаемый в этом потокеThreadLocalMapобъект.

    2) Судить о текущемThreadLocalMapсуществует ли:

  • Если есть, звонитеmap.setустановить этот объектentry.

  • Если его нет, звонитеcreateMapпровестиThreadLocalMapинициализация объекта, и эта сущностьentryкак первое значение, сохраненное вThreadLocalMapсередина.

Образец кода:

    /**
     * 设置当前线程对应的ThreadLocal的值
     * 大多数子类都不需要重写此方法,
     * 只需要重写 {@link #initialValue}方法代替设置当前线程对应的ThreadLocal的值
     *
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     *  
     */
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
            createMap(t, value);
    }

    /**
     * 为当前线程Thread 创建对应维护的ThreadLocalMap. 
     *
     * @param t the current thread 当前线程
     * @param firstValue 第一个要存放的ThreadLocal变量值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • перечислитьremove()Когда операция удаляет значение, хранящееся в ThreadLocal, соответствующем текущему потоку, выполняются следующие операции:

    1) Получить текущий потокThreadобъект, а затем получить объект потока, поддерживаемый в этом потокеThreadLocalMapобъект.

    2) Судить о текущемThreadLocalMapесть, если есть, звонитеmap.remove, с текущимThreadLocalзаkeyудалить соответствующий объектentry.

  • Образец кода:
      /**
       * 删除当前线程中保存的ThreadLocal对应的实体entry
       * 如果此ThreadLocal变量在当前线程中调用 {@linkplain #get read}方法
       * 则会通过调用{@link #initialValue}进行再次初始化,
       * 除非此值value是通过当前线程内置调用 {@linkplain #set set}设置的
       * 这可能会导致在当前线程中多次调用{@code initialValue}方法
       *
       * @since 1.5
       */
       public void remove() {
          // 获取当前线程对象中维护的ThreadLocalMap对象
           ThreadLocalMap m = getMap(Thread.currentThread());
          // 如果此map存在
           if (m != null)
              // 存在则调用map.remove
              // 以当前ThreadLocal为key删除对应的实体entry
               m.remove(this);
       }

    4.

    Вопрос: даThreadLocalОбщая операция на самом деле находится в потокеThreadсерединаThreadLocalMapоперация, ядроThreadLocalMapЭта хэш-таблица, вы можете говорить оThreadLocalMapВнутренняя базовая реализация ?

отвечать:

  • ThreadLocalMapБазовая реализация является пользовательскойHashMapХеш-таблица, основные элементы:

    1 ) Entry[] table;: базовая хэш-таблица, которую необходимо расширить при необходимости Длина базовой хеш-таблицы table.length должна быть равна 2 в n-й степени.

    2 ) int size;: количество элементов, фактически хранящихся в записях пары ключ-значение.

    3 ) int threshold;: Порог для следующего расширения, порог = длина базовой хеш-таблицы.len * 2 / 3. когдаsize >= thresholdпри пересеченииtableи удалитьkeyзаnullэлемент, если он удален послеsize >= threshold*3/4, необходимоtableдля расширения (подробности см.set(ThreadLocal<?> key, Object value)описание метода).

  • вEntry[] table;Основными элементами хранилища хеш-таблиц являютсяEntry,EntryВключают:

    1 ) ThreadLocal<?> k;: в настоящее время хранитсяThreadLocalэкземпляр объекта

    2 ) Object value;: текущий ThreadLocal соответствует сохраненному значению значения

  • Следует отметить, что этоEntryунаследованные слабые ссылкиWeakReference, поэтому используяThreadLocalMap, нашелkey == null, значит этоkey ThreadLocalбольше не упоминается, его нужно удалить изThreadLocalMapудалены из хеш-таблицы. (См. вопросы и ответы 5 для объяснения проблем, связанных со слабыми ссылками)

Образец кода:

    /**
     * ThreadLocalMap 是一个定制的自定义 hashMap 哈希表,只适合用于维护
     * 线程对应ThreadLocal的值. 此类的方法没有在ThreadLocal 类外部暴露,
     * 此类是私有的,允许在 Thread 类中以字段的形式声明 ,     
     * 以助于处理存储量大,生命周期长的使用用途,
     * 此类定制的哈希表实体键值对使用弱引用WeakReferences 作为key, 
     * 但是, 一旦引用不在被使用,
     * 只有当哈希表中的空间被耗尽时,对应不再使用的键值对实体才会确保被 移除回收。
     */
    static class ThreadLocalMap {

        /**
         * 实体entries在此hash map中是继承弱引用 WeakReference, 
         * 使用ThreadLocal 作为 key 键.  请注意,当key为null(i.e. entry.get()
         * == null) 意味着此key不再被引用,此时实体entry 会从哈希表中删除。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** 当前 ThreadLocal 对应储存的值value. */
            Object value;

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

        /**
         * 初始容量大小 16 -- 必须是2的n次方.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 底层哈希表 table, 必要时需要进行扩容.
         * 底层哈希表 table.length 长度必须是2的n次方.
         */
        private Entry[] table;

        /**
         * 实际存储键值对元素个数 entries.
         */
        private int size = 0;

        /**
         * 下一次扩容时的阈值
         */
        private int threshold; // 默认为 0

        /**
         * 设置触发扩容时的阈值 threshold
         * 阈值 threshold = 底层哈希表table的长度 len * 2 / 3
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * 获取该位置i对应的下一个位置index
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * 获取该位置i对应的上一个位置index
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

    }
  • ThreadLocalMapКонструктор загружается лениво, то есть только тогда, когда потоку необходимо сохранить соответствующийThreadLocalинициализируется и создается один раз (инициализируется только один раз). Шаги инициализации следующие:

    1) Инициализировать базовый массивtableНачальная вместимость 16.

    2) ПолучитьThreadLocalсерединаthreadLocalHashCode,пройти черезthreadLocalHashCode & (INITIAL_CAPACITY - 1), то есть хеш-значение ThreadLocal threadLocalHashCode % длина хэш-таблицы для вычисления места хранения объекта.

    3) Сохраните текущий объект, ключ: текущее значение ThreadLocal: значение, которое нужно сохранить

    4) Установите размер текущего фактического количества элементов хранения равным 1

    5) Установите порогsetThreshold(INITIAL_CAPACITY), что составляет 2/3 от начальной емкости 16.

Образец кода:


        /**
         * 用于创建一个新的hash map包含 (firstKey, firstValue).
         * ThreadLocalMaps 构造方法是延迟加载的,所以我们只会在至少有一个
         * 实体entry存放时,才初始化创建一次(仅初始化一次)。
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 初始化 table 初始容量为 16
            table = new Entry[INITIAL_CAPACITY];
            // 计算当前entry的存储位置
            // 存储位置计算等价于:
            // ThreadLocal 的 hash 值 threadLocalHashCode  % 哈希表的长度 length
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 存储当前的实体,key 为 : 当前ThreadLocal  value:真正要存储的值
            table[i] = new Entry(firstKey, firstValue);
            // 设置当前实际存储元素个数 size 为 1
            size = 1;
            // 设置阈值,为初始化容量 16 的 2/3。
            setThreshold(INITIAL_CAPACITY);
        }
  • ThreadLocalизget()Операция фактически вызываетThreadLocalMapизgetEntry(ThreadLocal<?> key)метод, этот метод быстро подходит для получения существованияkeyорганизацияentry, иначе он должен вызватьgetEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)Приобретение метода, чтобы максимизировать производительность прямых попаданий, метод выполняет следующие операции:

    1) Рассчитать, что получитьentryМесто хранения , вычисление места хранения эквивалентно:ThreadLocalизhashценностьthreadLocalHashCode% длины хеш-таблицыlength.

    2) Получить соответствующий объект в соответствии с рассчитанным местом храненияEntry. Определить соответствующий объектEntryсуществует иkeyРавно ли:

  • Есть соответствующий объектEntryи соответствуютkeyравный, такой жеThreadLocal, возвращает соответствующий объектEntry.

  • Соответствующий объект не существуетEntryилиkeyне равны, вызываяgetEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)метод, чтобы продолжать находить.

  • getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)Метод работает следующим образом:

    1) Получить базовый массив хеш-таблицtable, прокрутите объект, соответствующий поискуEntryсвязанное местоположение.

    2) Получить текущий пройденныйentryизkey ThreadLocal,сравниватьkeyЯвляется ли он последовательным или нет, он вернется, если он непротиворечив.

    3) Еслиkeyнепоследовательный иkeyзаnull, это доказывает, что ссылка больше не существует, потому чтоEntryунаследованоWeakReference, который является ямой, вызванной слабыми ссылками. перечислитьexpungeStaleEntry(int staleSlot)метод удаления объектов с истекшим сроком действияEntry(Этот метод отдельно не объясняется, пожалуйста, посмотрите пример кода, там есть подробные комментарии).

    4 ) keyнепоследовательный,keyЕсли он не пуст, перейдите к следующей позиции и продолжите поиск.

    5) После завершения обхода, если он все еще не найден, возвращаемсяnull.

Образец кода:


        /**
         * 根据key 获取对应的实体 entry.  此方法快速适用于获取某一存在key的
         * 实体 entry,否则,应该调用getEntryAfterMiss方法获取,这样做是为
         * 了最大限制地提高直接命中的性能
         *
         * @param  key 当前thread local 对象
         * @return the entry 对应key的 实体entry, 如果不存在,则返回null
         */
        private Entry getEntry(ThreadLocal<?> key) {
            // 计算要获取的entry的存储位置
            // 存储位置计算等价于:
            // ThreadLocal 的 hash 值 threadLocalHashCode  % 哈希表
            的长度 length
            int i = key.threadLocalHashCode & (table.length - 1);
            // 获取到对应的实体 Entry 
            Entry e = table[i];
            // 存在对应实体并且对应key相等,即同一ThreadLocal
            if (e != null && e.get() == key)
                // 返回对应的实体Entry 
                return e;
            else
                // 不存在 或 key不一致,则通过调用getEntryAfterMiss继续查找
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * 当根据key找不到对应的实体entry 时,调用此方法。
         * 直接定位到对应的哈希表位置
         *
         * @param  key 当前thread local 对象
         * @param  i 此对象在哈希表 table中的存储位置 index
         * @param  e the entry 实体对象
         * @return the entry 对应key的 实体entry, 如果不存在,则返回null
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            // 循环遍历当前位置的所有实体entry
            while (e != null) {
                // 获取当前entry 的 key ThreadLocal
                ThreadLocal<?> k = e.get();
               // 比较key是否一致,一致则返回
                if (k == key)
                    return e;
                // 找到对应的entry ,但其key 为 null,则证明引用已经不存在
                // 这是因为Entry继承的是WeakReference,这是弱引用带来的坑
                if (k == null)
                    // 删除过期(stale)的entry
                    expungeStaleEntry(i);
                else
                    // key不一致 ,key也不为空,则遍历下一个位置,继续查找
                    i = nextIndex(i, len);
                // 获取下一个位置的实体 entry
                e = tab[i];
            }
            // 遍历完毕,找不到则返回null
            return null;
        }


        /**
         * 删除对应位置的过期实体,并删除此位置后对应相关联位置key = null的实体
         *
         * @param staleSlot 已知的key = null 的对应的位置索引
         * @return 对应过期实体位置索引的下一个key = null的位置
         * (所有的对应位置都会被检查)
         */
        private int expungeStaleEntry(int staleSlot) {
            // 获取对应的底层哈希表 table
            Entry[] tab = table;
            // 获取哈希表长度
            int len = tab.length;

            // 擦除这个位置上的脏数据
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // 直到我们找到 Entry e = null,才执行rehash操作
            // 就是遍历完该位置的所有关联位置的实体
            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;

                        // 我们必须一直遍历直到最后
                        // 因为还可能存在多个过期的实体
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        /**
         * 删除所有过期的实体
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }
  • ThreadLocalизset(T value)Операция фактически вызываетThreadLocalMapизset(ThreadLocal<?> key, Object value)метод, который делает следующее:

    1) Получить соответствующую базовую хеш-таблицуtable, рассчитать соответствующийthrealocalместо хранения.

    2) пройти черезtableСущность, соответствующая местоположению, найдите соответствующийthreadLocal.

    3) Получить текущее местоположениеthreadLocal,еслиkey threadLocalнепротиворечиво, это доказывает, что соответствующиеthreadLocal, присваивая новое значение текущему найденному объектуEntryизvalue, конец.

    4) Если текущее местоположениеkey threadLocalнепоследовательный, иkey threadLocalзаnull, затем позвонитеreplaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)метод (этот метод отдельно не поясняется, посмотрите пример кода, там есть подробные комментарии), замените эту позициюkey == nullСущность — это сущность, которую нужно установить в данный момент, конец.

    5) Если текущее местоположениеkey threadLocalнепоследовательный, иkey threadLocalне дляnull, новый объект создается и сохраняется в текущей позиции itab[i] = new Entry(key, value);, который на самом деле хранит количество элементов пары ключ-значениеsize + 1, так как слабые ссылки вызывают эту проблему, поэтому вызовитеcleanSomeSlots(int i, int n)Метод очистки бесполезных данных (этот метод не объясняется отдельно, пожалуйста, проверьте пример кода, есть подробные комментарии), чтобы судить о текущемsizeБыл ли достигнут порогthreshhold, если данных для очистки нет, а количество элементов хранения все равно больше порогового, то вызовемrehashметод расширения (этот метод отдельно не объясняется, пожалуйста, проверьте пример кода, там есть подробные комментарии).

Образец кода:


        /**
         * 设置对应ThreadLocal的值
         *
         * @param key 当前thread local 对象
         * @param value 要设置的值
         */
        private void set(ThreadLocal<?> key, Object value) {

            // 我们不会像get()方法那样使用快速设置的方式,
            // 因为通常很少使用set()方法去创建新的实体
            // 相对于替换一个已经存在的实体, 在这种情况下,
            // 快速设置方案会经常失败。

            // 获取对应的底层哈希表 table
            Entry[] tab = table;
            // 获取哈希表长度
            int len = tab.length;
            // 计算对应threalocal的存储位置
            int i = key.threadLocalHashCode & (len-1);

            // 循环遍历table对应该位置的实体,查找对应的threadLocal
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                // 获取当前位置的ThreadLocal
                ThreadLocal<?> k = e.get();
                // 如果key threadLocal一致,则证明找到对应的threadLocal
                if (k == key) {
                    // 赋予新值
                    e.value = value;
                    // 结束
                    return;
                }
                // 如果当前位置的key threadLocal为null
                if (k == null) {
                    // 替换该位置key == null 的实体为当前要设置的实体
                    replaceStaleEntry(key, value, i);
                    // 结束
                    return;
                }
            }
            // 当前位置的k != key  && k != null
            // 创建新的实体,并存放至当前位置i
            tab[i] = new Entry(key, value);
            // 实际存储键值对元素个数 + 1
            int sz = ++size;
            // 由于弱引用带来了这个问题,所以先要清除无用数据,才能判断现在的size有没有达到阀值threshhold
            // 如果没有要清除的数据,存储元素个数仍然 大于 阈值 则扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                // 扩容
                rehash();
        }

        /**
         * 当执行set操作时,获取对应的key threadLocal,并替换过期的实体
         * 将这个value值存储在对应key threadLocal的实体中,无论是否已经存在体
         * 对应的key threadLocal
         *
         * 有一个副作用, 此方法会删除该位置下和该位置nextIndex对应的所有过期的实体
         *
         * @param  key 当前thread local 对象
         * @param  value 当前thread local 对象对应存储的值
         * @param  staleSlot 第一次找到此过期的实体对应的位置索引index
         *         .
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            // 获取对应的底层哈希表 table
            Entry[] tab = table;
            // 获取哈希表长度
            int len = tab.length;
            Entry e;

            // 往前找,找到table中第一个过期的实体的下标
            // 清理整个table是为了避免因为垃圾回收带来的连续增长哈希的危险
            // 也就是说,哈希表没有清理干净,当GC到来的时候,后果很严重

            // 记录要清除的位置的起始首位置
            int slotToExpunge = staleSlot;
            // 从该位置开始,往前遍历查找第一个过期的实体的下标
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // 找到key一致的ThreadLocal或找到一个key为 null的
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // 如果我们找到了key,那么我们就需要把它跟新的过期数据交换来保持哈希表的顺序
                // 那么剩下的过期Entry呢,就可以交给expungeStaleEntry方法来擦除掉
                // 将新设置的实体放置在此过期的实体的位置上
                if (k == key) {
                    // 替换,将要设置的值放在此过期的实体中
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // 如果存在,则开始清除之前过期的实体
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // 在这里开始清除过期数据
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // / 如果我们没有在往后查找中找没有找到过期的实体,
                // 那么slotToExpunge就是第一个过期Entry的下标了
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 最后key仍没有找到,则将要设置的新实体放置
            // 在原过期的实体对应的位置上。
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // 如果该位置对应的其他关联位置存在过期实体,则清除
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }


        /**
         * 启发式的扫描查找一些过期的实体并清除,
         * 此方法会再添加新实体的时候被调用, 
         * 或者过期的元素被清除时也会被调用.
         * 如果实在没有过期数据,那么这个算法的时间复杂度就是O(log n)
         * 如果有过期数据,那么这个算法的时间复杂度就是O(n)
         * 
         * @param i 一个确定不是过期的实体的位置,从这个位置i开始扫描
         *
         * @param n 扫描控制: 有{@code log2(n)} 单元会被扫描,
         * 除非找到了过期的实体, 在这种情况下
         * 有{@code log2(table.length)-1} 的格外单元会被扫描.
         * 当调用插入时, 这个参数的值是存储实体的个数,
         * 但如果调用 replaceStaleEntry方法, 这个值是哈希表table的长度
         * (注意: 所有的这些都可能或多或少的影响n的权重
         * 但是这个版本简单,快速,而且似乎执行效率还可以)
         *
         * @return true 返回true,如果有任何过期的实体被删除。
         */
        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }


        /**
         * 哈希表扩容方法
         * 首先扫描整个哈希表table,删除过期的实体
         * 缩小哈希表table大小 或 扩大哈希表table大小,扩大的容量是加倍.
         */
        private void rehash() {
            // 删除所有过期的实体
            expungeStaleEntries();

            // 使用较低的阈值threshold加倍以避免滞后
            // 存储实体个数 大于等于 阈值的3/4则扩容
            if (size >= threshold - threshold / 4)
                resize();
        }

        /**
         * 扩容方法,以2倍的大小进行扩容
         * 扩容的思想跟HashMap很相似,都是把容量扩大两倍
         * 不同之处还是因为WeakReference带来的
         */
        private void resize() {
            // 记录旧的哈希表
            Entry[] oldTab = table;
            // 记录旧的哈希表长度
            int oldLen = oldTab.length;
            // 新的哈希表长度为旧的哈希表长度的2倍
            int newLen = oldLen * 2;
            // 创建新的哈希表
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            // 逐一遍历旧的哈希表table的每个实体,重新分配至新的哈希表中
            for (int j = 0; j < oldLen; ++j) {
                // 获取对应位置的实体
                Entry e = oldTab[j];
                // 如果实体不会null
                if (e != null) {
                    // 获取实体对应的ThreadLocal
                    ThreadLocal<?> k = e.get(); 
                    // 如果该ThreadLocal 为 null
                    if (k == null) {
                        // 则对应的值也要清除
                        // 就算是扩容,也不能忘了为擦除过期数据做准备
                        e.value = null; // Help the GC
                    } else {
                        // 如果不是过期实体,则根据新的长度重新计算存储位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                       // 将该实体存储在对应ThreadLocal的最后一个位置
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            // 重新分配位置完毕,则重新计算阈值Threshold
            setThreshold(newLen);
            // 记录实际存储元素个数
            size = count;
            // 将新的哈希表赋值至底层table
            table = newTab;
        }
  • ThreadLocalизremove()Операция фактически вызываетThreadLocalMapизremove(ThreadLocal<?> key)метод, который делает следующее:

    1) Получить соответствующую базовую хеш-таблицуtable, рассчитать соответствующийthrealocalместо хранения.

    2) пройти черезtableСущность, соответствующая местоположению, найдите соответствующийthreadLocal.

    3) Получить текущее местоположениеthreadLocal,еслиkey threadLocalнепротиворечиво, это доказывает, что соответствующиеthreadLocal, выполните операцию удаления, удалите объект в этой позиции и завершите работу.

Образец кода:


        /**
         * 移除对应ThreadLocal的实体
         */
        private void remove(ThreadLocal<?> key) {
            // 获取对应的底层哈希表 table
            Entry[] tab = table;
            // 获取哈希表长度
            int len = tab.length;
            // 计算对应threalocal的存储位置
            int i = key.threadLocalHashCode & (len-1);
            // 循环遍历table对应该位置的实体,查找对应的threadLocal
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                // 如果key threadLocal一致,则证明找到对应的threadLocal
                if (e.get() == key) {
                    // 执行清除操作
                    e.clear();
                    // 清除此位置的实体
                    expungeStaleEntry(i);
                    // 结束
                    return;
                }
            }
        }

5.

просить:ThreadLocalMapобъекты хранения вEntryиспользоватьThreadLocalв видеkey, но этоEntryнаследуется слабая ссылкаWeakReferenceДа, почему он разработан таким образом, используя слабые ссылкиWeakReferenceНе приведет ли это к утечке памяти?

отвечать:

  • Во-первых, прежде чем ответить на этот вопрос, мне нужно объяснить, что такое сильная ссылка и что такое слабая ссылка.

В обычных обстоятельствах мы обычно используем сильные ссылки:

A a = new A();

B b = new B();

когдаa = null;b = null;, через некоторое время механизм сборки мусора JAVA GC вернет выделенное пространство памяти, соответствующее a и b.

Но рассмотрим такую ​​ситуацию:

C c = new C(b);
b = null;

когда b установлено вnullвремя, означает ли это, что работа GC может восстановить пространство памяти, выделенное b через определенный период времени? Ответ отрицательный, потому что даже если b установлено вnull, но c по-прежнему содержит ссылку на b, и это по-прежнему сильная ссылка, поэтому GC не будет восстанавливать пространство, первоначально выделенное b, которое не может быть ни освобождено, ни использовано, что вызывает утечку памяти.

Итак, как с этим бороться?

в состоянии пройтиc = null;, вы также можете использовать слабые ссылкиWeakReference w = new WeakReference(b);. из-за слабых ссылокWeakReference, GC может вернуть пространство, первоначально выделенное b.

Приведенное выше объяснение в основном относится к:Некоторые мысли о принципе реализации ThreadLocal

  • назадThreadLocalуровень,ThreadLocalMapиспользоватьThreadLocalслабая ссылка какkey, еслиThreadLocalНет внешней сильной ссылки, чтобы сослаться на него, тогда, когда система является GC, этоThreadLocalобязательно будет переработано, так что,ThreadLocalMapпоявится вkeyзаnullизEntry, к ним нет доступаkeyзаnullизEntryизvalue, если текущий поток снова задерживается, этиkeyзаnullизEntryизvalueВсегда будет цепочка сильных ссылок:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> valueЕго невозможно восстановить, что приводит к утечке памяти.

фактически,ThreadLocalMapЭта ситуация была учтена при проектировании, и были добавлены некоторые защитные меры:ThreadLocalизget(),set(),remove()очистит ветку, когдаThreadLocalMapвсе вkeyзаnullизvalue.

Но эти пассивные меры предосторожности не гарантируют от утечек памяти:

  • использоватьstaticизThreadLocal, расширенныйThreadLocalжизненный цикл, что может привести к утечкам памяти (ссылкаПример анализа утечки памяти ThreadLocal).

  • выделено с использованиемThreadLocalбольше не звонитьget(),set(),remove()метод, это вызовет утечку памяти.

На первый взгляд, источником утечки памяти является использование слабых ссылок. Большинство статей в Интернете посвящено анализуThreadLocalИспользование слабых ссылок может привести к утечкам памяти, но стоит задуматься и над другим вопросом: зачем использовать слабые ссылки вместо сильных?

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

To help deal with very large and long-lived usages, 
the hash table entries use WeakReferences for keys.

Для очень большого и длительного использования хеш-таблицы используют слабоссылочныеkey.

Ниже мы обсудим два случая:

  • keyИспользуйте сильные ссылки: в кавычкахThreadLocalОбъект переработан, ноThreadLocalMapтакже держатьThreadLocalсильные ссылки, если их не удалить вручную,ThreadLocalне перерабатывается, в результате чегоEntryУтечка памяти.

  • keyИспользование слабых ссылок: ссылкаThreadLocalОбъект утилизирован из-заThreadLocalMapдержатьThreadLocalслабых ссылок, даже без ручного удаления,ThreadLocalтакже будут переработаны.valueв следующий разThreadLocalMapперечислитьget(),set(),remove()будет очищен.

  • Сравнивая два случая, мы можем обнаружить, что:ThreadLocalMapжизненный цикл сThreadТой же длины, если вручную не удалить соответствующийkey, приведет к утечке памяти, но использование слабых ссылок может обеспечить дополнительный уровень защиты: слабые ссылкиThreadLocalНе будет утечки памяти, соответствующейvalueв следующий разThreadLocalMapперечислитьget(),set(),remove()будет очищен.

следовательно,ThreadLocalИсточник утечки памяти: из-заThreadLocalMapжизненный цикл сThreadдо тех пор, пока не удалить вручную соответствующийkeyприведет к утечке памяти, а не из-за слабых ссылок.

На основании вышеприведенного анализа мы можем понятьThreadLocalПричины и последствия утечек памяти, так как же избежать утечек памяти?

после каждого использованияThreadLocal, оба называют этоremove()метод очистки данных.

В случае использования пула потоков он вовремя не очищаетсяThreadLocal, это не только проблема утечек памяти, но и более серьезные проблемы с бизнес-логикой. Итак, используйтеThreadLocalТочно так же, как разблокировка после блокировки, очистите его после использования.

Приведенное выше объяснение в основном относится к:Углубленный анализ утечек памяти ThreadLocal

6.

просить:ThreadLocalиsynchronizedразница?

отвечать:ThreadLocalиsynchronizedОба ключевых слова используются для решения проблемы одновременного доступа к переменным несколькими потоками, но суть и идеи у них разные.

  1. ThreadLocalЭто класс Java, который разрешает конфликт доступа к переменным в разных потоках, оперируя локальными переменными в текущем потоке. так,ThreadLocalПредоставляет потокобезопасный механизм общих объектов, каждый поток владеет его копией.

  2. на ЯвеsynchronizedЭто зарезервированное слово, которое зависит от механизма блокировки JVM для достижения атомарности доступа к функциям или переменным в критической секции. В механизме синхронизации механизм блокировки объекта гарантирует, что только один поток одновременно обращается к переменной. В этом случае переменная, используемая в качестве «механизма блокировки», совместно используется несколькими потоками.

  • Механизм синхронизации (synchronizedключевое слово) использует метод «время вместо пространства», чтобы предоставить переменную для разных потоков, ставящихся в очередь для доступа. иThreadLocalМетод «обмена пространства на время» используется для предоставления каждому потоку копии переменной, чтобы обеспечить одновременный доступ, не влияя друг на друга.

7.

просить:ThreadLocalКаковы текущие сценарии применения?

Ответ: В целомThreadLocalВ основном для решения 2 типов проблем:

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

  • Чтобы решить проблемы с хранением данных:ThreadLocalКопия переменной создается в каждом потоке, поэтому каждый поток может получить доступ к своей внутренней копии переменной, и разные потоки не будут мешать друг другу. какParameterДанные объекта необходимо использовать в нескольких модулях.Если будет принят метод передачи параметров, это, очевидно, увеличит связь между модулями. В этот момент мы можем использоватьThreadLocalрешать.

Сценарии применения:

SpringиспользоватьThreadLocalИсправление проблем с безопасностью потоков

  • Мы знаем, что в основном только лица без гражданстваBeanмогут использоваться только в многопоточной среде, вSpringподавляющего большинстваBeanможет быть объявлен какsingletonсфера. это потому чтоSpringнекоторымBean(какRequestContextHolder,TransactionSynchronizationManager,LocaleContextHolderи т. д.) в непоточно-безопасном состоянии, принявThreadLocalпроцесс, чтобы сделать их потокобезопасными, потому что с сохранением состоянияBeanОн может быть разделен между несколькими потоками.

  • ОбщееWebПриложение разделено на три уровня: уровень представления, уровень обслуживания и уровень сохраняемости.Соответствующая логика написана в разных слоях, и нижний уровень открывает вызов функции на верхний уровень через интерфейс. Как правило, все вызовы программы от получения запроса до возврата ответа принадлежат одному и тому же потоку.ThreadLocalЭто хорошая идея для решения проблемы безопасности потоков, которая решает конфликт одновременного доступа к переменным, предоставляя каждому потоку независимую копию переменной. Во многих случаях,ThreadLocalчем использовать напрямуюsynchronizedМеханизм синхронизации проще и удобнее для решения проблемы безопасности потоков, а полученная программа имеет более высокий параллелизм.

Образец кода:

public abstract class RequestContextHolder  {
····

    private static final boolean jsfPresent =
            ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());

    private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
            new NamedThreadLocal<RequestAttributes>("Request attributes");

    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
            new NamedInheritableThreadLocal<RequestAttributes>("Request context");

·····
}

Суммировать

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

  2. ThreadLocalДизайн: каждыйThreadподдерживать одинThreadLocalMapхэш-таблица, хэш-таблицаkeyдаThreadLocalсам экземпляр,valueфактическое значение, которое нужно сохранитьObject.

  3. правильноThreadLocalОбщая операция на самом деле находится в потокеThreadсерединаThreadLocalMapработать.

  4. ThreadLocalMapБазовая реализация является пользовательскойHashMapхеш-таблица,ThreadLocalMapпорогthreshold= базовая хеш-таблицаtableдлинаlen * 2 / 3, когда фактическое количество хранимых элементовsizeбольше или равно порогуthresholdиз3/4Времяsize >= threshold*3/4, то базовый массив хеш-таблицtableВыполнение операций расширения.

  5. ThreadLocalMapхэш-таблица вEntry[] tableОсновными элементами хранения являютсяEntry, хранитсяkeyдаThreadLocalобъект экземпляра,valueдаThreadLocalсоответствует сохраненному значениюvalue. Следует отметить, что этоEntryунаследованные слабые ссылкиWeakReference, поэтому используяThreadLocalMap, нашелkey == null, значит этоkey ThreadLocalбольше не упоминается, его нужно удалить изThreadLocalMapудалены из хеш-таблицы.

  6. ThreadLocalMapиспользоватьThreadLocalслабая ссылка какkey, еслиThreadLocalНет внешней сильной ссылки, чтобы сослаться на него, тогда, когда система является GC, этоThreadLocalобязаны быть переработаны. Итак, вThreadLocalизget(),set(),remove()очистит ветку, когдаThreadLocalMapвсе вkeyзаnullизvalue. Если мы не будем активно вызывать вышеупомянутую операцию, это вызовет утечку памяти.

  7. для безопасного использованияThreadLocal, его необходимо разблокировать после каждого использования замка, после каждого использованияThreadLocalзвонить послеremove()чтобы убрать бесполезноеEntry. Это особенно важно, когда операция использует пул потоков.

  8. ThreadLocalиsynchronizedОтличие: механизм синхронизации (synchronizedключевое слово) использует метод «время вместо пространства», чтобы предоставить переменную для разных потоков, ставящихся в очередь для доступа. иThreadLocalМетод «обмена пространства на время» используется для предоставления каждому потоку копии переменной, чтобы обеспечить одновременный доступ, не влияя друг на друга.

  9. ThreadLocalВ основном для решения 2 типов проблем: A. Для решения проблем параллелизма: используйтеThreadLocalРешайте проблемы параллелизма вместо механизмов синхронизации. B. Решить проблемы с хранением данных: какParameterДанные объекта необходимо использовать в нескольких модулях.Если будет принят метод передачи параметров, это, очевидно, увеличит связь между модулями. В этот момент мы можем использоватьThreadLocalрешать.

Справочная статья

Подробное объяснение ThreadLocal
Разница между ThreadLocal и синхронизированным?
Глубокое погружение в ThreadLocal
Внутренний механизм ThreadLocal
Давайте поговорим о безопасности потоков в Spring
Некоторые мысли о принципе реализации ThreadLocal
Углубленный анализ утечек памяти ThreadLocal
Изучите базовые знания Java, которые должен изучить Spring (6) --- ThreadLocal
Шаблон проектирования ThreadLocal
Тематическое исследование ThreadLocal
Шаблон Spring singleton и безопасность потоков ThreadLocal