Thread在管理request作用域的Bean、事务管理、任务调度、AOP等模块中都有它的身影,所以想了解Spring事务管理的底层技术,ThreadLocal是必须攻克的“山头堡垒”。
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路,使用这个工具类可以很简洁地编写出优美的多线程程序。
ThreadLocal,顾名思义,它不是一个线程,而且保存线程本地化对象的容器。当运行于多线程环境的某个对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以没和线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
InheritableThreadLocal 继承于 ThreadLocal,它自动为子线程复制一份从父线程那里继承而来的本地变量:在创建子线程时,子线程会接收所有可继承的线程本地变量的初始值。当必须将本地线程变量自动传送给所有创建的子线程时,应尽可能地使用InheritableThreadLocal,而非ThreadLocal。
ThreadLocal主要是4个public方法,其他的方法都是辅助这4个方法。
/**
* 返回此线程局部变量的当前线程副本中的值。如果变量没有当前线程的值,
* 则首先将其初始化为调用 initialvalue 方法返回的值。
* @return 此线程的当前线程的本地值
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
/**
* 将此线程局部变量的当前线程副本设置为指定值。大多数子类将不需要重写这个方法,
* 仅仅依靠 initialvalue 方法来设置线程局部变量的值。
* @param value 要存储在此线程的当前线程本地副本中的值。
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* 删除此线程局部变量的当前线程值。如果此线程局部变量随后被当前线程@linkplain读取,
* 则通过调用其 initialvalue 方法重新初始化其值,除非其值是 linkplain 由临时中的当前线程设置。
* 这可能导致在当前线程中多次调用 initialvalue 方法。
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* 创建线程局部变量。变量的初始值通过调用upplier上的get方法来确定。
* @param线程局部值的类型
* @参数supplier用于确定初始值的供应商
* @返回新的线程局部变量
* @如果supplier为空,则引发NullPointerException
*
* @since 1.8
*/
public static ThreadLocal withInitial(Supplier extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
我们可以看到,里面有一个内部数据结构ThreadLocalMap
虽然叫ThreadLocalMap,但是其并没有实现Map接口,其内部是自己实现的一个Entry对象,以及kye,value格式。
我们看下其重要方法:
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
注意
/**
* 初始容量
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 表,大小必须是2的幂。
*/
private Entry[] table;
/**
* 表大小
*/
private int size = 0;
/**
* 要调整大小的下一个大小值。
*/
private int threshold; // Default to 0
/**
* 阈值,设置最坏是长度的2/3。
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
作为一个map,肯定避免不了hash冲突以及扩容问题。那么ThreadLocalMap是如何实现的。
/**
* 增加
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 减少
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
我们可以看到ThreadLocalMap使用的是最简单,步长加1或减1,寻找下一个相邻的位置。也是线性探测。
线性探测:
根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
上面介绍ThreadLocal 以及 ThreadLocalMap就暂时介绍完了。
我们下面说下ThreadLocalMap缺点:
因为是使用的是线性探测法,步长+1或者-1,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
再说问题产生原因和解决办法前,我们先说下为什么要使用弱引用:
从表面上看,发生内存泄漏,是因为Key使用了弱引用类型。但其实是因为整个Entry的key为null后,没有主动清除value导致。很多文章大多分析ThreadLocal使用了弱引用会导致内存泄漏,但为什么使用弱引用而不是强引用?
官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。
下面我们分两种情况讨论:
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。
ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,当ThreadLocal没有外部强引用来引用它的时候,ThreadLocal会在下次JVM垃圾收集时被回收。
这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。
但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。
在get()方法中调用map.getEntry(this)时,其内部会判断key是否为null,继续看map.getEntry(this)源码:
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 判断Entry是否为空,以及key是否为null
if (e != null && e.get() == key)
return e;
// key为空调用getEntryAfterMiss()
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal> k = e.get();
if (k == key)
return e;
// 如果key == null,调用expungeStaleEntry
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 设置value = null,删除value,便于下次回收。
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
// 循环检查,判断是否有key == null 的存在,如果有,一并将其value 设置为 null,方便回收
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;
// 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不会发生内存泄漏,例如:
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
我们可能还听说过线程同步机制。它也是为了解决多线程中相同变量的访问冲突问题。那么二者相比有什么不同呢。