这是我参与更文挑战的第7天,活动详情查看: 更文挑战
基本介绍
ThreadLocal
类是java.lang
包下的一个类,用于线程内部的数据存储,通过它可以在指定的线程中存储数据,本文针对该类进行原理分析。
使用场景
我们一般用ThreadLocal来提供线程局部变量。线程局部变量会在每个Thread内拥有一个副本,Thread只能访问自己的那个副本。
用例
public class ThreadLocalTest {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
protected Integer initialValue(){
return 0;
}
};
// 值传递
@Test
public void testValue(){
for (int i = 0; i < 5; i++){
new Thread(() -> {
Integer temp = threadLocal.get();
threadLocal.set(temp + 5);
System.out.println("current thread is " + Thread.currentThread().getName() + " num is " + threadLocal.get());
}, "thread-" + i).start();
}
}
}
复制代码
以上程序的输出结果是:
current thread is thread-1 num is 5
current thread is thread-3 num is 5
current thread is thread-0 num is 5
current thread is thread-4 num is 5
current thread is thread-2 num is 5
复制代码
我们可以看到,每一个线程打印出来的都是5,哪怕我是先通过ThreadLocal.get()
方法获取变量,然后再set
进去,依然不会进行重复叠加。
这就是线程隔离。
源码分析
ThreadLocal
set
/**
* 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.
*/
public void set(T value) {
//获得当前线程
Thread t = Thread.currentThread();
//获取该线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
//将数据放入ThreadLocalMap中,key是当前ThreadLocal对象,值是我们传入的value。
map.set(this, value);
else
//初始化ThreadLocalMap,并以当前ThreadLocal对象为Key,value为值存入map中。
createMap(t, value);
}
复制代码
ThreadLocal#set()
方法主要是通过当前线程的ThreadLocalMap
实现的。ThreadLocalMap
是一个Map,它的key是ThreadLoacl
,value是Object
。
get
/**
* 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
*/
public T get() {
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//若该线程的ThreadLocalMap对象已存在,则直接获取该Map里的值;否则则通过初始化函数创建1个ThreadLocalMap对象
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
复制代码
大体上与set方法类似,就是先获取到当前线程的ThreadLocalMap
,然后以this为key可以取得value
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.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
复制代码
到这里我们基本上明白了ThreadLocal
的工作原理,我们总结一下
-
每个
Thread
实例内部都有一个ThreadLocalMap
,ThreadLocalMap
是一种Map,它的key是ThreadLocal
,value是Object。 -
ThreadLocal#set()
方法其实是往当前线程的ThreadLocalMap
中存入数据,其key是当前ThreadLocal
对象,value是set方法中传入的值。 -
使用数据时,以当前
ThreadLocal
为key,从当前线程的ThreadLocalMap
中取出数据。
ThreadLocalMap
ThreadLocal
的核心就是ThreadLocalMap
,它是维护我们线程与变量之间关系的一个类。
Entry内部类
static class ThreadLocalMap {
/**
* 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.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
* Entry数组的初始化大小
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
* <ThreadLocal, 保存的泛型值>数组
* 长度必须是2的N次幂
* 这个可以参考为什么HashMap里维护的数组也必须是2的N次幂
* 主要是为了减少碰撞,能够让保存的元素尽量的分散
* 关键代码还是hashcode & table.length - 1
*/
private Entry[] table;
/**
* The number of entries in the table.
* table里的元素个数
*/
private int size = 0;
/**
* The next size value at which to resize.
* 扩容的阈值
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
* 根据长度计算扩容的阈值
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Increment i modulo len.
* 获取下一个索引,超出长度则返回0
*/
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);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table的大小为16
table = new Entry[INITIAL_CAPACITY];
// 通过hashcode & (长度-1)的位运算,确定键值对的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 创建一个新节点保存在table当中
table[i] = new Entry(firstKey, firstValue);
// 设置table内元素为1
size = 1;
// 设置扩容阈值
setThreshold(INITIAL_CAPACITY);
}
/**
* Construct a new map including all Inheritable ThreadLocals
* from given parent map. Called only by createInheritedMap.
*
* ThreadLocal本身是线程隔离的,按道理是不会出现数据共享和传递的行为的
* 这是InheritableThreadLocal提供了了一种父子间数据共享的机制
* @param parentMap the map associated with parent thread.
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
}
复制代码
- 我们可以看到,在
ThreadLocalMap
这个内部类当中,又定义了一个Entry
内部类,它是一个WeakReference
准去的说继承自弱引用 - ThreadLocal对象的引用被传到
WeakReference
的reference
中,entry.get()
被当作map元素的key,而Entry还多了一个字段value
,用来存放ThreadLocal变量实际的值。 - 至于为什么要用弱引用呢?我想我源码上面的注释其实也写得很明白了,这ThreadLocal实际上就是个线程本地变量隔离作用的工具类而已,当线程走完了,肯定希望能回收这部分产生的资源,所以就用了弱引用。
- 由于是弱引用,若ThreadLocal对象不再有普通引用,GC发生时会将ThreadLocal对象清除。而Entry的key,即
entry.get()
会变为null。然而,GC只会清除被引用对象,Entry
还被线程的ThreadLocalMap引用着,因而不会被清除。因而,value
对象就不会被清除。除非线程退出,造成该线程的ThreadLocalMap整体释放,否则value
的内存就无法释放,内存泄漏! - JDK的作者自然想到了这一点,因此在ThreadLocalMap的很多方法中,调用
expungeStaleEntries()
清除entry.get() == null
的元素,将Entry的value释放。所以,只要线程还在使用其他ThreadLocal,已经无效的ThreadLocal内存就会被清除。 - 然而,我们大部分的使用场景是,ThreadLocal是一个静态变量,因此永远有普通引用指向每个线程中的ThreadLocalMap的该entry。因此该ThreadLocal的Entry永远不会被释放,自然
expungeStaleEntries()
就无能为力,value
的内存也不会被释放。所以在我们确实用完了ThreadLocal后,可以主动调用remove()
方法,主动删掉entry。
我相信有人会有疑问,如果在我要用的时候,被回收了怎么办?下面的代码会一步步地让你明白,你考虑到的问题,这些大牛都已经想到并且解决了。接着看
getEntry && getEntryAfterMiss
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
// 通过hashcode确定下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果找到则直接返回
if (e != null && e.get() == key)
return e;
else
// 找不到的话接着从i位置开始向后遍历,基于线性探测法,是有可能在i之后的位置找到的
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 循环向后遍历
while (e != null) {
// 获取节点对应的k
ThreadLocal<?> k = e.get();
// 相等则返回
if (k == key)
return e;
// 如果为null,触发一次连续段清理
if (k == null)
expungeStaleEntry(i);
else
// 获取下一个下标接着进行判断
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
复制代码
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
*
* @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).
* 这个函数可以看做是ThreadLocal里的核心清理函数,它主要做的事情就是
* 1、从staleSlot开始,向后遍历将ThreadLocal对象被回收所在Entry节点的value和Entry节点本身设置null,方便GC,并且size自减1
* 2、并且会对非null的Entry节点进行rehash,只要不是在当前位置,就会将Entry挪到下一个为null的位置上
* 所以实际上是对从staleSlot开始做一个连续段的清理和rehash操作
*/
private int expungeStaleEntry(int staleSlot) {
// 新的引用指向table
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 先将传过来的下标置null
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;// table的size-1
// Rehash until we encounter null
Entry e;
int i;
// 遍历删除指定节点所有后续节点当中,ThreadLocal被回收的节点
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 获取entry当中的key
ThreadLocal<?> k = e.get();
// 如果ThreadLocal为null,则将value以及数组下标所在位置设置null,方便GC
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 重新计算key的下标
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// 如果是当前位置则遍历下一个
// 不是当前位置,则重新从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;
}
复制代码
这可以说是ThreadLocal
非常核心的一个清理方法,为什么会需要清理呢?或许很多人想不明白,我们用List或者是Map也好,都没有说要清理里面的内容。
但是这里是对于线程来说的隔离的本地变量,并且使用的是弱引用,那便有可能在GC的时候就被回收了。
-
如果有很多Entry节点已经被回收了,但是在table数组中还留着位置,这时候不清理就会浪费资源
-
在清理节点的同时,可以将后续非空的Entry节点重新计算下标进行排放,这样子在get的时候就能快速定位资源,加快效率。
set
/**
* Set the value associated with key.
* ThreadLocalMap的set方法,这个方法还是挺关键的
* 通过这个方法,我们可以看出该哈希表是用线性探测法来解决冲突的
* @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.
// 新开一个引用指向table
Entry[] tab = table;
// 获取table的长度
int len = tab.length;
// 获取对应ThreadLocal在table当中的下标
int i = key.threadLocalHashCode & (len-1);
/**
* 从该下标开始循环遍历
* 1、如遇相同key,则直接替换value
* 2、如果该key已经被回收失效,则替换该失效的key
*/
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
// 如果 k 为null,则替换当前失效的k所在Entry节点
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找到空的位置,创建Entry对象并插入
tab[i] = new Entry(key, value);
// table内元素size自增
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
复制代码
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
// 新开一个引用指向table
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).
// 记录当前失效的节点下标
int slotToExpunge = staleSlot;
/**
* 通过这个for循环的prevIndex(staleSlot, len)可以看出
* 这是由staleSlot下标开始向前扫描
* 查找并记录最前位置value为null的下标
*/
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
/**
* 通过for循环nextIndex(staleSlot, len)可以看出
* 这是由staleSlot下标开始向后扫描
*/
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对应,直接赋值value
* 则直接替换i与staleSlot两个下标
*/
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
// 通过注释看出,i之前的节点里,没有value为null的情况
if (slotToExpunge == staleSlot)
slotToExpunge = i;
/**
* 在调用cleanSomeSlots进行启发式清理之前
* 会先调用expungeStaleEntry方法从slotToExpunge到table下标所在为null的连续段进行一次清理
* 返回值便是table[]为null的下标
* 然后以该下标--len进行一次启发式清理
* 最终里面的方法实际上还是调用了expungeStaleEntry
* 可以看出expungeStaleEntry方法是ThreadLocal核心的清理函数
*/
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
/**
* 如果当前下标所在已经失效,并且向后扫描过程当中没有找到失效的Entry节点
* 则slotToExpunge赋值为当前位置
*/
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 如果并没有在table当中找到该key,则直接在当前位置new一个Entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
/**
* 在上面的for循环探测过程当中
* 如果发现任何无效的Entry节点,则slotToExpunge会被重新赋值
* 就会触发连续段清理和启发式清理
*/
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
复制代码
/**
* 启发式地清理被回收的Entry
* i对应的Entry是非无效的,有可能是失效被回收了,也有可能是null
* 会有两个地方调用到这个方法
* 1、set方法,在判断是否需要resize之前,会清理并rehash一遍
* 2、替换失效的节点时候,也会进行一次清理
*/
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];
// Entry对象不为空,但是ThreadLocal这个key已经为null
if (e != null && e.get() == null) {
n = len;
removed = true;
/**
* 调用该方法进行回收
* 实际上不是只回收 i 这一个节点而已
* 而是对 i 开始到table所在下标为null的范围内,对那些节点都进行一次清理和rehash
*/
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
复制代码
/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
/**
* 从下标0开始,逐个向后遍历插入到新的table当中
* 1、如遇到key已经为null,则value设置null,方便GC回收
* 2、通过hashcode & len - 1计算下标,如果该位置已经有Entry数组,则通过线性探测向后探测插入
*/
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
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
size = count;
// 指向新的Entry数组
table = newTab;
}
复制代码
以上的代码就是调用set方法往ThreadLocalMap
当中保存K-V关系的一系列代码,我就不分开再一个个讲了,这样大家看起来估计也比较方便,有连续性。
我们可以来看看一整个的set流程:
1、先通过hashcode & (len – 1)来定位该ThreadLocal
在table当中的下标i
2、从i开始for循环向后遍历
1)如果获取Entry节点的key与我们需要操作的ThreadLocal
相等,则直接替换value
2)如果遍历的时候拿到了key为null的情况,则调用replaceStaleEntry
方法进行与之替换。
3、如果上述两个情况都是,则直接在计算的出来的下标当中new一个Entry阶段插入。
4、进行一次启发式地清理并且如果插入节点后的size大于扩容的阈值,则调用resize方法进行扩容。
remove
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 将引用设置null,方便GC
e.clear();
// 从该位置开始进行一次连续段清理
expungeStaleEntry(i);
return;
}
}
}
复制代码
我们可以看到,remove节点的时候,也会使用线性探测的方式,当找到对应key的时候,就会调用clear将引用指向null,并且会触发一次连续段清理。
总结
借用张流程图,来自 ThreadLocal理解
内存分析
不知从何时起,网上开始流传ThreadLocal有内存泄漏的问题。下面我们从ThreadLocal的内存入手,分析一下这种说法是否正确。话不多说直接上图。
现在,我们假设ThreadLocal完成了自己的使命,与ThreadLocalRef断开了引用关系。此时内存图变成了这样。
系统GC发生时,由于Heap中的ThreadLocal只有来自key的弱引用,因此ThreadLocal内存会被回收到。
到这里,value被留在了Heap中,而我们没办法通过引用访问它。value这块内存将会持续到线程结束。导致内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。这样就能 一定情况下避免内存泄漏。
private void set(ThreadLocal<?> key, Object value) {
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
...
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
}
复制代码
ThreadLocal get方法获取时,有一段如果Entry的key为null,移除Entry和Entry.value的代码
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
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--;
}
...
}
return i;
}
复制代码
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
- 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
如果不想依赖线程的生命周期,那就调用remove方法来释放value的内存吧。个人认为,这种设计应该也是JDK开发大佬的无奈之举。
还可以参考Java进阶(七)正确理解Thread Local的原理与适用场景
其他知识点
为什么使用弱引用
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析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调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal 最佳实践
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
- 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
跨线程传递
ThreadLocal 如何解决 Hash 冲突?
我们知道,HashMap是一种get、set都非常高效的集合,它的时间复杂度只有O(1)。但是如果存在严重的Hash冲突,那HashMap的效率就会降低很多。
与 HashMap 不同,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始 key 的 hashcode 值( key.threadLocalHashCode & (len-1))确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。len是当前Entry[]的长度,这没什么好说的。那看来秘密就在threadLocalHashCode中了。我们来看一下threadLocalHashCode是如何产生的。
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
复制代码
这段代码非常简单。有个全局的计数器nextHashCode,每有一个ThreadLocal产生这个计数器就会加0x61c88647,然后把当前值赋给threadLocalHashCode。关于0x61c88647这个神奇的常量,可以点这里。
为什么ThreadLocal类内部的ThreadLocalMap要用Entry数组实现?
set方法只会设置一个值,直接用一个Entry不就解决问题了?为什么会涉及到数组,还会有数组扩容的问题?
首先明确一下ThreadLocal
的set
和get
的过程
-
调用
get()
- 获取当前线程
Thread
对象,进而获取此线程对象中维护的ThreadLocalMap
对象。 - 判断当前的
ThreadLocalMap
是否存在,如果存在,则以当前的ThreadLocal
为key
,调用ThreadLocalMap
中的getEntry
方法获取对应的存储实体e
。找到对应的存储实体e
,获取存储实体e
对应的value
值,即为我们想要的当前线程对应此ThreadLocal
的值,返回结果值。 - 如果不存在,则证明此线程没有维护的
ThreadLocalMap
对象,调用setInitialValue
方法进行初始化。返回setInitialValue
初始化的值。
- 获取当前线程
-
调用
set(T value)
- 获取当前线程
Thread
对象,进而获取此线程对象中维护的ThreadLocalMap
对象。 - 判断当前的
ThreadLocalMap
是否存在: - 如果存在,则调用
map.set
设置此实体entry
。 - 如果不存在,则调用
createMap
进行ThreadLocalMap
对象的初始化,并将此实体entry
作为第一个值存放至ThreadLocalMap
中。
- 获取当前线程
总结起来就是
ThreadLocal#get() 方法中首先取当前线程
然后从当前线程对象中获取 threadLocals 属性
这个属性就是 ThreadLocalMap 的一个对象
既然一个线程只对应一个 Thread 对象
那么一个线程中也就只有一个 ThreadLocalMap 对象
而一个程序中可以创建多个 ThreadLocal 对象
在同一个线程中都会访问到这个 Map
key就是一个指向threadLocal实例的一个弱引用
多个ThreadLocal实例的话,那么就key必然是不同的
那么这个 Map 就必须存储多个 value 值并考虑扩容
ThreadLocal实现原理是什么
通常,如果我不去看源代码的话,我猜ThreadLocal
是这样子设计的:每个ThreadLocal
类都创建一个Map
,然后用线程的ID threadID
作为Map
的key
,要存储的局部变量作为Map
的value
,这样就能达到各个线程的值隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal
就是这样设计的。
但是,JDK后面优化了设计方案,现时JDK8 ThreadLocal
的设计是:每个Thread
维护一个ThreadLocalMap
哈希表,这个哈希表的key
是ThreadLocal
实例本身,value
才是真正要存储的值Object
。
这个设计与我们一开始说的设计刚好相反,这样设计有如下几点优势:
- 这样设计之后每个
Map
存储的Entry
数量就会变小,因为之前的存储数量由Thread
的数量决定,现在是由ThreadLocal
的数量决定。 - 当
Thread
销毁之后,对应的ThreadLocalMap
也会随之销毁,能减少内存的使用。
与同步机制区别
类型 | 实现机制 | 同步共享 | 应用场景 |
---|---|---|---|
ThreadLocal | 为每个线程都提供一个变量的副本,从而实现同时访问,而互不影响。 (以空间换时间) |
无需对该变量进行同步 (每个线程都拥有自己的变量副本) |
隔离多个线程的数据共享 |
同步机制 | 提供一个变量,让不同的线程排队访问 (以时间换空间) |
被作用”锁机制”的变量是多个线程共享的 – 通过对象的锁机制保证同一时间只有一个线程访问变量 – synchronized = 1个保留字,依靠JVM的锁机制来实现临界区的函数or变量的访问中的原子性 |
同步多个线程对相同资源的并发访问,防止并发冲突 |
更多参考ThreadLocal和synchronized的区别?
参考
Java多线程:神秘的线程变量 ThreadLocal 你了解吗?
ThreadLocal和ThreadLocalMap源码分析