Зачем использовать ThreadLocal
Официально говоря, это способ добиться закрытия потока, например, многопоточный доступ к общим переменным, переменным-членам класса, безопасны в однопоточной среде, и только один поток может их использовать. Но в случае многопоточности он будет использоваться несколькими потоками, и ThreadLocal должен гарантировать, что поток использует только свои собственные переменные-члены.
Простой в использовании
В приложении ThreadLocal наиболее распространенным сценарием является приобретение соединений с базой данных.Давайте подумаем.Если мы будем поддерживать здесь общее глобальное соединение, то в случае параллелизма предыдущий поток завершится и закроет соединение. Но текущая ситуация все еще интересует.
private static final String DB_URL = "localhost:3306";
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* <p>This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
@Override
protected Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
private static Connection getConnection(){
return connectionHolder.get();
}
Анализ исходного кода
Я всегда считал, что самое главное при просмотре исходного кода — понимать аннотации, поэтому я не буду удалять аннотации прямо здесь, а проанализирую их вместе с аннотациями.
аннотация класса
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* 这段话其实就说明了ThreadLocal的作用。线程自己的,独立初始化的变量副本
*
* <p>For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
* <pre>
* 这里说明了,用id去标识线程。包括产生ID的方法。
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // Atomic integer containing the next thread ID to be assigned
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // Thread local variable containing each thread's ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // Returns the current thread's unique ID, assigning it if necessary
* public static int get() {
* return threadId.get();
* }
* }
* </pre>
* <p>Each thread holds an implicit reference to its copy of a thread-local
* variable as long as the thread is alive and the {@code ThreadLocal}
* instance is accessible; after a thread goes away, all of its copies of
* thread-local instances are subject to garbage collection (unless other
* references to these copies exist).
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
Мы открываем исходный код ThreadLocal, и там есть этот комментарий к его классу. Когда мы смотрим на это, мы должны уловить ключевые моменты.На самом деле, в этом абзаце говорится о двух вещах: что делает ThreadLocal и как генерируется ThreadId.
Структура ThreadLocal
Следует отметить:
- Сам экземпляр ThreadLocal не хранит значения, он просто предоставляет ключ для поиска копии в текущем потоке.
- Это ThreadLocal содержится в Thread, а не Thread содержится в ThreadLocal
- Каждый поток имеет свой собственный ThreadLocalMap.
- Когда каждый поток помещает значение в ThreadLocal, он сохраняет его в своем собственном ThreadLocalMap.Чтение также использует ThreadLocal в качестве ссылки и находит соответствующий ключ в своей собственной карте, таким образом реализуя изоляцию потока.
Входная структура
Запись представляет собой структуру k-v, где ключ — ThreadLocal, аслабая ссылка. Эта WeakReference (слабая ссылка) является причиной утечки памяти ThreadLocal.
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*
* entry.get() == null的时候是脏key
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal.set()
Далее, давайте сначала рассмотрим общие методы set(value) и get().
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
* 将此线程局部变量的当前线程副本设置为指定值。 大多数子类将不需要重写此方法,而仅依靠initialValue方法来设置线程initialValue的值。
*/
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//如果map不为空就放在ThreadLocalMap里面 ThreadLocal为key
map.set(this, value);
else
//否则创建一个ThreadLocalMap
createMap(t, value);
}
Здесь видно, что реализация в ThreadLocal очень проста, и большая ее часть — это работа ThreadLocalMap, так что на самом деле операция ThreadLocal — это работа с ThreadLocalMap.
getMap()
Реализация кода здесь состоит только из одной строки, которая должна удалить threadLocals текущего потока.
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
//拿出来当前线程的threadLocals
return t.threadLocals;
}
ThreadLocalMap.set()
Код здесь в основном для установки значения. Он включает в себя структуру хранилища k-v, алгоритм разрешения конфликтов и обработку восстановленных объектов.
/**
* Set the value associated with key.
* key为ThreadLocal value 就是实际设定的值
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
//这里可以看出它是用Entry来实现k-v存储的,稍后会讲Entry的实现
Entry[] tab = table;
//获取tab的长度
int len = tab.length;
//通过threadLocalHashCode去计算存储的位置
int i = key.threadLocalHashCode & (len-1);
//这里是一个开放定址法解决hash碰撞
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//获取tab[i]
ThreadLocal<?> k = e.get();
//如果里面已经有ThreadLocal并且相等就直接覆盖
if (k == key) {
e.value = value;
return;
}
//如果当前位置为null(ThreadLocal被回收了) 就进行替换
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果是没有相同ThreadLocal元素,并且当前位置没有被回收的ThreadLocal就直接new Entry(key,value)
tab[i] = new Entry(key, value);
//增加size
int sz = ++size;
//清除被回收的元素,并且如果sz大于等于threshold(阈值)进行rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalHashCode
ThreadLocalHashCode на самом деле является пользовательским хэш-кодом, базовой реализацией является AtomicInteger, использующий unsafe для атомарных операций.
/**
* ThreadLocals rely on per-thread linear-probe hash maps attached
* to each thread (Thread.threadLocals and
* inheritableThreadLocals). The ThreadLocal objects act as keys,
* searched via threadLocalHashCode. This is a custom hash code
* (useful only within ThreadLocalMaps) that eliminates collisions
* in the common case where consecutively constructed ThreadLocals
* are used by the same threads, while remaining well-behaved in
* less common cases.
* 说明了threadLocalHashCode的作用通过threadLocalHashCode搜索ThreadLocal对象
*/
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
* threadLocalHashCode是一个AtomicInteger,他的操作时原子的,并且从0开始
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
* 这里可以看作一个自增的步长,把线性的id转换为用于2的幂次方表的近似最佳分布的乘法哈希值。
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
* 返回下一个code
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
replaceStaleEntry
Этот метод более сложен, и здесь следует отметить, что процесс всего метода заключается в попытке стереть некоторые найденные staleSlots. Вы можете увидеть такой вопрос здесь.Так как ключ уже пустой, он должен быть переработан GC.Зачем вам специально проводить такой процесс? Поскольку ключ здесь перерабатывается, значение не перерабатывается, а запись не перерабатывается, поэтому их необходимо перерабатывать. Они не будут расширены в первую очередь и будут обсуждаться позже.
/**
* Replace a stale entry encountered during a set operation
* with an entry for the specified key. The value passed in
* the value parameter is stored in the entry, whether or not
* an entry already exists for the specified key.
*
* As a side effect, this method expunges all stale entries in the
* "run" containing the stale entry. (A run is a sequence of entries
* between two null slots.)
*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
//向前遍历找到找到第一个脏key(e.get() == null)
//这个操作是为了把前面的脏Entry在一次过程中一起释放出来
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
//向后遍历,找到后边第一个脏key和上面的循环顺序相反
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
//如果发现这个key已经存在
if (k == key) {
//覆盖value
e.value = value;
//与之前的脏key交换
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
//如果在往前查找的过程中没发现脏key(slotToExpunge== staleSlot)
if (slotToExpunge == staleSlot)
//设置slotToExpunge = 当前位置
slotToExpunge = i;
//进行清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果在往前查找的过程中没发现脏key
// 那么我们需要把slotToExpunge 设置为当前位置
if (k == null && slotToExpunge == staleSlot)
//设置slotToExpunge = 当前位置
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 如果key 在数组中没有存在,那么直接新建一个新的放进去就可以
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果有其他已经过期的对象,那么需要清理他
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
Из исходного кода мы можем знать, что весь процесс можно разделить на две части.ждать с нетерпениема такжеоглядываться. Поскольку, если грязный ключ появляется в одном месте, очень вероятно, что грязный ключ появится в соседнем месте, поэтому для повышения эффективности этот метод будет обрабатывать смежные грязные ключи вместе. Вот мы и рассматриваем эти ситуации.
Примечание. Оценка грязного ключа в этой статье (e = tab[i]) != null && e.get() == null
- Найдите грязные ключи вперед и найдите ключи, которые можно перезаписать (k == key)
2. Найти грязный ключ вперед, но не найти ключ, который можно перезаписать (k != ключ)
3. Грязный ключ не найден вперед, а ключ, который можно перезаписать, найден назад (k == ключ)Логика здесь немного сложнее, просто запомните два момента, 1. Если вы найдете грязный ключ, оглядываясь назад, cleanSomeSlots — это местонахождение грязных ключей. 2. Если грязный ключ не найден при обратном поиске, cleanSomeSlots — текущая позиция i.
- Грязный ключ не найден в прямом направлении, ключ, который можно перезаписать, не найден в обратном направлении (k != ключ)
Цель замены переопределенного ключа здесь состоит в том, чтобы предотвратить появление двух идентичных ключей.
expungeStaleEntry
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
* 清除脏key
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
//清除当前脏key
tab[staleSlot].value = null;
tab[staleSlot] = null;
//减少tab size
size--;
// Rehash until we encounter null
//重新hash直到遇到null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//遇到脏key 清理
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//rehash
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
Следовательно, после выполнения этого метода между позицией staleSlot~i нет грязного ключа.
cleanSomeSlots
Здесь всего два параметра, на которые стоит обратить внимание:
- i — это возвращаемое значение вызова expungeStaleEntry, позиция i не должна быть грязным ключом, поэтому поиск начинается со следующей позиции i.
- n является параметром управления сканированием, и битовая операция выполняется над n для управления циклом.
/**
* Heuristically scan some cells looking for stale entries.
* This is invoked when either a new element is added, or
* another stale one has been expunged. It performs a
* logarithmic number of scans, as a balance between no
* scanning (fast but retains garbage) and a number of scans
* proportional to number of elements, that would find all
* garbage but would cause some insertions to take O(n) time.
*
* 此方法在添加和删除的时候调用
* 此方法有可能会导致插入的时间复杂度变成O(n)
*
* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
* 从i位置之后开始
* @param n scan control: {@code log2(n)} cells are scanned,
* unless a stale entry is found, in which case
* 扫描控制
* {@code log2(table.length)-1} additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
* 如果没有找到脏key 就会继续扫描log2(n)次也就是log2(table.length)-1个元素
* @return true if any stale entries have been removed.
* 如果删除了脏key 就会返回 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];
//判断是不是脏key
if (e != null && e.get() == null) {
n = len;
removed = true
//调用处理脏key的方法
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0); // 向右位移一位相当于相当于n除以2
return removed;
}
ThreadLocal.get()
Далее мы рассмотрим метод ThreadLocal.get(). Метод get намного проще и понятнее, чем метод set. Нам просто нужно сосредоточиться на методе setInitialValue.
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
* 返回当前线程中的值,如果不存在就调用setInitialValue方法
*/
public T get() {
//拿取当前线程
Thread t = Thread.currentThread();
//获取当前线程的map
ThreadLocalMap map = getMap(t);
//map不为空
if (map != null) {
//获取当前线程的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
//Entry不为空
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//调用初始化方法
return setInitialValue();
}
setInitialValue
Этот метод также очень прост, вы можете обнаружить, что он похож на метод set(), о котором мы упоминали ранее. Здесь нужно обратить внимание на метод initialValue()
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
initialValue
Вы можете видеть, что комментарии к этому методу очень длинные, в основном объясняющие несколько моментов:
- Этот метод вызывается при первом вызове get() без вызова set().
- Обычно этот метод вызывается не более одного раза для каждого потока, но может вызываться снова в случае последующих вызовов для удаления и последующего получения.
- Если начальное значение требования не равно нулю, вы должны создать подкласс ThreadLocal и переопределить этот метод.
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* <p>This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
ThreadLocal.remove()
Нам просто нужно сосредоточиться на методе remove() и содержании этого комментария.
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* 如果删除这个当前线程元素之后马上调用get()会导致initialValue多次调用
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove
Следует отметить, что expungeStaleEntry вызывается для очистки грязных ключей после удаления.
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//获取当前线程的hash
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//找到目标Entry
if (e.get() == key) {
//删除Entry
e.clear();
//删除脏key
expungeStaleEntry(i);
return;
}
}
}
rehash
Все мы знаем, что HashMap и ArrayList имеют свой собственный механизм расширения, ThreadLocalMap на самом деле имеет свой собственный механизм расширения, его кодовая реализация находится в методе rehash. В коде set() мы знаем, что перефразирование выполняется, когда (sz >= threshold).
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
* 首先执行expungeStaleEntries扫描整个表的脏key,清空脏key,在判断是否需要扩容
*/
private void rehash() {
//扫表,清除所有脏key
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
//用一个较低的阈值去判断是否要扩容,这里是 size >= 3/4 threshold
if (size >= threshold - threshold / 4)
//扩容方法
resize();
}
expungeStaleEntries
Вот работа по очистке всей таблицы
/**
* Expunge all stale entries in the table.
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
//遍历表
for (int j = 0; j < len; j++) {
Entry e = tab[j];
//判断是否为脏key
if (e != null && e.get() == null)
//调用清理过程
expungeStaleEntry(j);
}
}
resize
Это касается только удвоения емкости.
/**
* Double the capacity of the table.
* 说明了是两倍扩容
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
//创建新的Entry[]大小为原来的两倍
Entry[] newTab = new Entry[newLen];
int count = 0;
//遍历老Entry[]
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
//如果key为空
if (k == null) {
//把value也设为空,防止内存泄漏
e.value = null; // Help the GC
} else {
//计算位置
int h = k.threadLocalHashCode & (newLen - 1);
//如果位置被占用 开放定址法
while (newTab[h] != null)
h = nextIndex(h, newLen);
//设置位置
newTab[h] = e;
count++;
}
}
}
//设置阈值
setThreshold(newLen);
size = count;
table = newTab;
}
открытая адресация
Так называемый метод открытой адресации заключается в поиске следующего пустого хеш-адреса после возникновения конфликта.Пока хеш-таблица достаточно велика, пустой хэш-адрес всегда можно найти и запись сохраняется. Здесь мы кратко поговорим о линейном обнаружении в методе открытой адресации, который также используется в нашем ThreadLocal.
Линейный метод обнаружения
Метод линейного обнаружения, как следует из названия, заключается в том, что функция разрешения конфликтов является линейной функцией, а самый прямой из них заключается в том, что такая функция разрешения конфликтов также используется в коде TreadLocal.
f(x)= x+1
Но следует отметить, что в TreadLocal это кольцеобразный щуп, если он достигнет границы, то сразу пересечет границу на другой конец.
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
Преимущества линейного метода обнаружения:
- Нет лишнего места (по сравнению с методом застежки-молнии, который требует дополнительного связанного списка)
- Последовательность зондирования локализована и может использовать преимущества системных кешей, сокращая количество операций ввода-вывода (непрерывных адресов памяти).
недостаток:
- Затраты времени > O(1) (наихудший O(n))
- Увеличение конфликтов - прошлые конфликты приведут к последующим цепным конфликтам (временная сложность приближается к O (n))
Зачем использовать слабые ссылки
Выше мы упоминали, что Entry в ThreadLocal использует слабые ссылки, давайте посмотрим, почему мы используем слабые ссылки.
Из эталонного отношения мы видим, что ссылка Entry.key указывает на ThreadLocal — это пунктирная линия, то есть слабая ссылка.Когда наш ThreadLocal равен нулю, из-за отсутствия строгого отношения ссылки он будет правильно переработан сборщиком мусора .
Например этот кусок кода:
public class test {
private void threadLocalTest(){
ThreadLocal<String> threadLocal = new ThreadLocal<>();
System.out.println(threadLocal.get());
}
}
Когда выполнение моего метода threadLocalTest завершится, объект threadLocal будет переработан, но если ThreadLocal и Entry.key являются не слабыми ссылками, а сильными ссылочными связями, объект threadLocal не будет переработан сборщиком мусора, что приведет к утечке памяти. Он будет переработан только тогда, когда поток умрет.
Конечно, использование слабых ссылок не может полностью предотвратить утечку памяти, потому что у значения все еще есть проблема с утечкой памяти. Все грязные ключи будут очищены, когда мы set(), get().
Предлагаемое использование ThreadLocal:
- Он спроектирован как статический, на него строго ссылаются объекты класса, и он не будет повторно использоваться во время выживания потока.
- Он спроектирован так, чтобы быть нестатичным, и внутренняя часть длинных объектов (например, объектов, управляемых пружиной) не будет перерабатываться.
- Лучше не создавать объекты ThreadLocal в методах
Наконец, на самом деле слабые ссылки также могут повысить эффективность использования памяти JVM.Это очень полезно, если мы не будем внедрять сторонний кеш, такой как Redis, а сделаем кеш JDK сами.
О 0x61c88647
Как мы видим в предыдущем коде, значение роста алгоритма Hash в коде равно 0x61c88647, что является очень особенным значением.
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
* 这里可以看作一个自增的步长,把线性的id转换为用于2的幂次方表的近似最佳分布的乘法哈希值。
*/
private static final int HASH_INCREMENT = 0x61c88647;
Как мы уже говорили ранее, проблема с методом линейного обнаружения заключается в том, что, как только происходит столкновение, очень вероятно, что столкновение будет происходить каждый раз, что приведет к серии сбоев. А использование значения 0x61c88647 в качестве значения роста хэша может в определенной степени решить эту проблему, благодаря чему сгенерированное значение более равномерно распределяется в массиве размера степени 2. То есть, когда мы используем 0x61c88647 в качестве размера шага для накопления и присвоения каждому ThreadLocal собственного идентификатора, то есть threadLocalHashCode, а затем по модулю степени 2, полученные результаты распределяются очень равномерно.
Выбор 0x61c88647 на самом деле связан с хешированием Фибоначчи, которое является математическим знанием и не будет здесь подробно останавливаться.
TODO
- Реальный бой Threadlocal
личный блог
Уровень автора ограничен, если есть ошибки или неточности, просьба указывать.
Справочная статья
2.Статья, подробно объясняющая проблему утечки памяти ThreadLocal из исходного кода.
3.Зачем использовать 0x61c88647
4.Открытая адресация — линейное зондирование
Справочная литература
- «Практика параллельного программирования на Java»