предисловие
ThreadLocal
Многие студенты не понимают, что это такое и для чего его можно использовать. Но его часто спрашивают на собеседованиях, так что в этот раз я буду учиться вместе с вами.ThreadLocal
этот класс.
Ниже я узнаю наши в виде вопросов интервью-ThreadLocal
Класс (анализ исходного кода основан на JDK8)
Эта статья одновременно публикуется в Цзяньшу:у-у-у. Краткое описание.com/afraid/807686414…
Вопросы и ответы
1.
просить:ThreadLocal
понял? Можете ли вы рассказать мне о его основном использовании?
отвечать:
-
Из официальной пары JAVA
ThreadLocal
Иллюстративное определение класса (определено в примере кода):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, и текущий JDK8
ThreadLocal
Дизайн: каждыйThread
поддерживать одинThreadLocalMap
хеш-таблица, хэш-таблицаkey
даThreadLocal
сам экземпляр,value
фактическое значение, которое нужно сохранитьObject
. -
Этот дизайн прямо противоположен тому, о чем мы говорили в начале, он имеет следующие преимущества:
1) После этого дизайна каждый
Map
хранитсяEntry
сумма будет меньше, потому что предыдущая сумма хранения определяетсяThread
определяется количествомThreadLocal
количество определяется.2) Когда
Thread
После разрушения соответствующиеThreadLocalMap
Он также будет уничтожен, что может уменьшить использование памяти.
Приведенное выше объяснение в основном относится к:Разница между 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
Оба ключевых слова используются для решения проблемы одновременного доступа к переменным несколькими потоками, но суть и идеи у них разные.
-
ThreadLocal
Это класс Java, который разрешает конфликт доступа к переменным в разных потоках, оперируя локальными переменными в текущем потоке. так,ThreadLocal
Предоставляет потокобезопасный механизм общих объектов, каждый поток владеет его копией. -
на Яве
synchronized
Это зарезервированное слово, которое зависит от механизма блокировки JVM для достижения атомарности доступа к функциям или переменным в критической секции. В механизме синхронизации механизм блокировки объекта гарантирует, что только один поток одновременно обращается к переменной. В этом случае переменная, используемая в качестве «механизма блокировки», совместно используется несколькими потоками.
- Механизм синхронизации (
synchronized
ключевое слово) использует метод «время вместо пространства», чтобы предоставить переменную для разных потоков, ставящихся в очередь для доступа. иThreadLocal
Метод «обмена пространства на время» используется для предоставления каждому потоку копии переменной, чтобы обеспечить одновременный доступ, не влияя друг на друга.
7.
просить:ThreadLocal
Каковы текущие сценарии применения?
Ответ: В целомThreadLocal
В основном для решения 2 типов проблем:
-
Чтобы решить проблемы параллелизма: используйте
ThreadLocal
заменятьsynchronized
для обеспечения безопасности резьбы. Механизм синхронизации использует подход «время вместо пространства», в то время какThreadLoca
l Принят метод «обмена пространства временем». Первый предоставляет переменную только для разных потоков, ставящихся в очередь на доступ, а второй предоставляет переменную для каждого потока, поэтому к ним можно обращаться одновременно, не влияя друг на друга. -
Чтобы решить проблемы с хранением данных:
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");
·····
}
Суммировать
-
ThreadLocal
Предоставляйте локальные переменные в потоке, к которым можно получить доступ в любое время и в любом месте в этом потоке, изолируя другие потоки. -
ThreadLocal
Дизайн: каждыйThread
поддерживать одинThreadLocalMap
хэш-таблица, хэш-таблицаkey
даThreadLocal
сам экземпляр,value
фактическое значение, которое нужно сохранитьObject
. -
правильно
ThreadLocal
Общая операция на самом деле находится в потокеThread
серединаThreadLocalMap
работать. -
ThreadLocalMap
Базовая реализация является пользовательскойHashMap
хеш-таблица,ThreadLocalMap
порогthreshold
= базовая хеш-таблицаtable
длинаlen * 2 / 3
, когда фактическое количество хранимых элементовsize
больше или равно порогуthreshold
из3/4
Времяsize >= threshold*3/4
, то базовый массив хеш-таблицtable
Выполнение операций расширения. -
ThreadLocalMap
хэш-таблица вEntry[] table
Основными элементами хранения являютсяEntry
, хранитсяkey
даThreadLocal
объект экземпляра,value
даThreadLocal
соответствует сохраненному значениюvalue
. Следует отметить, что этоEntry
унаследованные слабые ссылкиWeakReference
, поэтому используяThreadLocalMap
, нашелkey == null
, значит этоkey ThreadLocal
больше не упоминается, его нужно удалить изThreadLocalMap
удалены из хеш-таблицы. -
ThreadLocalMap
использоватьThreadLocal
слабая ссылка какkey
, еслиThreadLocal
Нет внешней сильной ссылки, чтобы сослаться на него, тогда, когда система является GC, этоThreadLocal
обязаны быть переработаны. Итак, вThreadLocal
изget()
,set()
,remove()
очистит ветку, когдаThreadLocalMap
все вkey
заnull
изvalue
. Если мы не будем активно вызывать вышеупомянутую операцию, это вызовет утечку памяти. -
для безопасного использования
ThreadLocal
, его необходимо разблокировать после каждого использования замка, после каждого использованияThreadLocal
звонить послеremove()
чтобы убрать бесполезноеEntry
. Это особенно важно, когда операция использует пул потоков. -
ThreadLocal
иsynchronized
Отличие: механизм синхронизации (synchronized
ключевое слово) использует метод «время вместо пространства», чтобы предоставить переменную для разных потоков, ставящихся в очередь для доступа. иThreadLocal
Метод «обмена пространства на время» используется для предоставления каждому потоку копии переменной, чтобы обеспечить одновременный доступ, не влияя друг на друга. -
ThreadLocal
В основном для решения 2 типов проблем: A. Для решения проблем параллелизма: используйтеThreadLocal
Решайте проблемы параллелизма вместо механизмов синхронизации. B. Решить проблемы с хранением данных: какParameter
Данные объекта необходимо использовать в нескольких модулях.Если будет принят метод передачи параметров, это, очевидно, увеличит связь между модулями. В этот момент мы можем использоватьThreadLocal
решать.
Справочная статья
Подробное объяснение ThreadLocal
Разница между ThreadLocal и синхронизированным?
Глубокое погружение в ThreadLocal
Внутренний механизм ThreadLocal
Давайте поговорим о безопасности потоков в Spring
Некоторые мысли о принципе реализации ThreadLocal
Углубленный анализ утечек памяти ThreadLocal
Изучите базовые знания Java, которые должен изучить Spring (6) --- ThreadLocal
Шаблон проектирования ThreadLocal
Тематическое исследование ThreadLocal
Шаблон Spring singleton и безопасность потоков ThreadLocal