一、简介
ThreadLocal
提供了线程间数据隔离的功能,从它的命名上也能知道这是属于一个线程的本地变量。也就是说,每个线程都会在 ThreadLocal
中保存一份该线程独有的数据,所以它是线程安全的。ThreadLocal
的作用域就是线程。
一个简单的例子展示ThreadLocal
的特性
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 创建一个有3个核心线程数的线程池
ExecutorService threadPool = new ThreadPoolExecutor(3, 3, 1, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10));
// 线程池提交一个任务,将任务序号及执行该任务的子线程的线程名放到 ThreadLocal 中
threadPool.execute(() -> threadLocal.set("任务1: " + Thread.currentThread().getName()));
threadPool.execute(() -> threadLocal.set("任务2: " + Thread.currentThread().getName()));
threadPool.execute(() -> threadLocal.set("任务3: " + Thread.currentThread().getName()));
// 输出 ThreadLocal 中的内容
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> System.out.println("ThreadLocal value of " + Thread.currentThread().getName() + " = " + threadLocal.get()));
}
// 线程池记得关闭
threadPool.shutdown();
}
}
运行结果如下:
由此可见,线程池中执行任务的线程,随后多次输出线程在 ThreadLocal
变量中存储的的内容也表明:每个线程在 ThreadLocal
中存储的内容是当前线程独有的,在多线程环境下,能够有效防止自己的变量被其他线程修改(存储的内容是同一个引用类型对象的情况除外)。
二、ThreadLocal 实现原理
底层数据结构
ThreadLocal
底层是通过ThreadLocalMap
这个静态内部类来存储数据的,ThreadLocalMap
可以理解为ThreadLocal
类实现的定制化的 HashMap
,它的底层是 Entry
对象数组,Entry
对象中存放的键是 ThreadLocal
对象,值是 Object
类型的具体存储内容。除此之外,ThreadLocalMap
也是 Thread
类一个属性,可以在Thread
类源码中找到。
主要方法
1.get方法
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
ThreadLocalMap map = getMap(t);
// 若 threadLocals 变量不为空,根据 ThreadLocal 对象来获取 key 对应的 value
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 若 threadLocals 变量是 NULL,初始化一个新的 ThreadLocalMap 对象
return setInitialValue();
}
// 初始化一个新的 ThreadLocalMap 对象
private T setInitialValue() {
// 初始化一个 NULL 值
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// ThreadLocalMap#createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
通过 ThreadLocal#get()
方法可以很清晰的看到,我们根据 ThreadLocal
对象从ThreadLocal
中读取数据时,首先会获取当前线程对象,然后得到当前线程对象中 ThreadLocal.ThreadLocalMap
类型的 threadLocals
属性;
如果 threadLocals
属性不为空,会根据 ThreadLocal
对象作为 key
来获取对应的 value
;如果 threadLocals
变量是 NULL
,就初始化一个新的ThreadLocalMap
对象。
再看 ThreadLocalMap
的构造方法,也就是 Thread
类中 ThreadLocal.ThreadLocalMap
类型的 threadLocals
属性不为空时的执行逻辑。
// ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
这个构造方法其实是将 ThreadLocal
对象作为 key
,存储的具体内容 Object
对象作为 value
,包装成一个 Entry
对象,放到 ThreadLocalMap
类中类型为 Entry
数组的 table
属性中,这样就完成了线程局部变量的存储。所以说, ThreadLocal 中的数据最终是存放在 ThreadLocalMap 这个类中的 。
2.set方法
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
ThreadLocalMap map = getMap(t);
// 若 threadLocals 变量不为空,进行赋值;否则新建一个 ThreadLocalMap 对象来存储
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {
// 获取 ThreadLocalMap 的 Entry 数组对象
Entry[] tab = table;
int len = tab.length;
// 获取当前 ThreadLocal 对象的散列值
int i = key.threadLocalHashCode & (len-1);
// 解决哈希冲突,线性探测法
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 代码(1)
if (k == key) {
e.value = value;
return;
}
// 代码(2)
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 代码(3)将 key-value 包装成 Entry 对象放在数组退出循环时的位置中
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// ThreadLocalMap#nextIndex
// Entry 数组的下一个索引,若超过数组大小则从0开始,相当于环形数组
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
ThreadLocalMap
类的底层数据结构是一个 Entry
类型的数组,但与 HashMap
中的 Node
数组+链表形式不同的是,Entry
类没有 next
属性来构成链表,没有像HashMap
一样使用拉链法解决哈希碰撞,而是采用线性探测法。
解决哈希冲突的循环中:
- 代码(1),如果当前
ThreadLocal
对象正好等于Entry
对象中的key
属性,直接更新ThreadLocal
中value
的值; - 代码(2),如果当前
ThreadLocal
对象不等于Entry
对象中的key
属性,并且Entry
对象的key
是空的,这里进行的逻辑其实是 设置键值对,同时清理无效的 Entry (防止内存泄漏); - 代码(3),如果在遍历中没有发现当前
TheadLocal
对象的散列值,也没有发现Entry
对象的key
为空的情况,而是满足了退出循环的条件,即Entry
对象为空时,那么就会创建一个 新的 Entry 对象进行存储 ,同时做一次 启发式清理 ,将Entry
数组中key
为空,value
不为空的对象的value
值释放;
ThreadLocalMap扩容机制
它在扩容前有两个判断的步骤,都满足后才会进行最终扩容。
-
ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中可能会触发启发式清理,在清理无效 Entry 对象后,如果数组长度大于等于数组定义长度的 2/3,则首先进行 rehash;
// rehash 条件 private void setThreshold(int len) { threshold = len * 2 / 3; }
-
rehash 会触发一次全量清理,如果数组长度大于等于数组定义长度的 1/2,则进行 resize(扩容);
// 扩容条件 private void rehash() { expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); }
-
进行扩容时,Entry 数组为扩容为 原来的2倍 ,重新计算 key 的散列值,如果遇到 key 为 NULL 的情况,会将其 value 也置为 NULL,帮助虚拟机进行GC。
// 具体的扩容函数 private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; 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 = count; table = newTab; }
父子线程间局部变量传递
实现线程间局部变量传递使用InheritableThreadLocal 类。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
先用一个简单的示例来实践一下父子线程间局部变量的传递功能。
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("这是父线程设置的值");
new Thread(() -> System.out.println("子线程输出:" + threadLocal.get())).start();
}
// 输出内容
子线程输出:这是父线程设置的值
可以看到,在子线程中通过调用 InheritableThreadLocal#get() 方法,拿到了在父线程中设置的值。
实现父子线程间的局部变量共享需要追溯到 Thread 对象的构造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
// 该参数一般默认是 true
boolean inheritThreadLocals) {
// 省略大部分代码
Thread parent = currentThread();
// 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// 省略部分代码
}
在最终执行的构造方法中,有这样一个判断:如果当前父线程(创建子线程的线程)的 inheritableThreadLocals
属性不为 NULL
,就会将当下父线程的 inheritableThreadLocals
属性复制给子线程的 inheritableThreadLocals
属性。具体的复制方法如下:
// ThreadLocal#createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 一个个复制父线程 ThreadLocalMap 中的数据
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) {
// childValue 方法调用的是 InheritableThreadLocal#childValue(T parentValue)
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++;
}
}
}
}
需要注意的是,复制父线程共享变量的时机是在创建子线程时,如果在创建子线程后父线程再往 InheritableThreadLocal 类型的对象中设置内容,将不再对子线程可见。
三、内存泄漏分析
发生内存泄漏的原因
ThreadLocal 发生内存泄漏的原因需要从 Entry 对象说起
// ThreadLocal->ThreadLocalMap->Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry
对象的 key
即 ThreadLocal
类是继承于 WeakReference
弱引用类。具有弱引用的对象有更短暂的生命周期,在发生 GC
活动时,无论内存空间是否足够,垃圾回收器都会回收具有弱引用的对象。
由于 Entry
对象的 key
是继承于 WeakReference
弱引用类的,若 ThreadLocal
类没有外部强引用,当发生 GC
活动时就会将 ThreadLocal
对象回收。
而此时如果创建 ThreadLocal
类的线程依然活动,那么 Entry
对象中 ThreadLocal
对象对应的 value
就依旧具有强引用而不会被回收,从而导致内存泄漏。
如何解决内存泄漏问题
要想解决内存泄漏问题其实很简单,只需要记得在使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以了。
ThreadLocal 内部如何防止内存泄漏
ThreadLocalMap#set(ThreadLocal<?> key, Object value)
其实已经有涉及 ThreadLocal
内部清理无效 Entry
的逻辑了,在通过线性检测法处理哈希冲突时,若 Entry
数组的 key
与当前 ThreadLocal
不是同一个对象,同时 key
为空的时候,会进行清理无效 Entry
的处理,即 ThreadLOcalMap#replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot)
方法:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
- 这个方法中也是一个循环,循环的逻辑与
ThreadLocalMap#set(ThreadLocal<?> key, Object value)
方法一致; - 在循环过程中如果找到了将要存储的
ThreadLocal
对象,则会将它与进入replaceStaleEntry
方法时满足条件的 k 值做交换,同时将value
更新; - 如果没有找到将要存储的
ThreadLocal
对象,则会在此 k 值处新建一个Entry
对象存储; - 同时,在循环过程中如果发现其他无效的
Entry
(key
为NULL
,value
还在的情况,可能导致内存泄漏,下文会有详细描述),会顺势找到Entry
数组中所有的无效Entry
,释放这些无效Entry
(通过将key
和value
都设置为NULL
),在一定程度上避免了内存泄漏;
如果满足线性检测循环结束条件了,即遇到了 Entry==NULL 的情况,就新建一个 Entry 对象来存储数据。然后会进行一次启发式清理,如果启发式清理没有成功释放满足条件的对象,同时满足扩容条件时,会执行 ThreadLocalMap#rehash() 方法。
private void rehash() {
// 全量清理
expungeStaleEntries();
// 满足条件则扩容
if (size >= threshold - threshold / 4)
resize();
}
ThreadLocalMap#rehash()
方法中会对 ThreadLocalMap
进行一次全量清理,全量清理会遍历整个 Entry
数组,删除所有 key
为 NULL
,value
不为 NULL
的脏 Entry对象。
// 全量清理
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);
}
}
进行全量清理之后,如果 Entry
数组的大小大于等于 threshold - threshold / 4
,则会进行2倍扩容。
总结一下:在ThreadLocal
内部是通过在 get、set、remove 方法中主动进行清理 key
为 NULL
且 value
不为 NULL
的无效 Entry 来避免内存泄漏问题。
但是基于 get、set 方法让 ThreadLocal
自行清理无效 Entry
对象并不能完全避免内存泄漏问题,要彻底解决内存泄漏问题还得养成使用完就主动调用remove
方法释放资源的好习惯。
四、ThreadLocal 应用场景及示例
ThreadLocal
在很多开源框架中都有应用,比如:Spring
中的事务隔离级别的实现
Spring
采用Threadlocal
的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection
对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring
框架里面就是用的ThreadLocal
来实现这种隔离,主要是在TransactionSynchronizationManager
这个类里面,代码如下所示:
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
……
还有就是很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。