ThreadLocal
ThreadLocal是一个线程本地变量,作用主要是做数据隔离。ThreadLocal中填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,每个线程维护自己的变量副本,多个线程互相不可见,因此多线程操作该变量不必加锁,适合不同线程使用不同变量值的场景。
数据隔离有什么用,用在哪些场景?
一个典型的应用场景就是Spring事务,事务是和线程绑定起来的,Spring框架在事务开始时会给当前线程绑定一个JDBC Connection,整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。对ThreadLocal的使用主要是在TransactionSynchronizationManager这个类里面,代码如下:
public abstract class TransactionSynchronizationManager {
//线程绑定的资源,比如DataSourceTransactionManager绑定是的某个数据源的一个Connection
//在整个事务执行过程中都使用同一个Jdbc Connection
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");
//事务只读属性
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
//事务隔离级别
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
//事务同步开启
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
//......
}
关于使用ThreadLocal解决持久层线程安全问题,在Spring在SingleTon模式下的线程安全这篇博客中也有分析。其实很多场景的cookie、session等数据隔离都是通过ThreadLocal去做实现的。
ThreadLocal用法及源码分析
ThreadLocal使用示例代码如下:
ThreadLocal<String> localName = new ThreadLocal();
localName.set("abcde");
String name = localName.get();
localName.remove();
先初始化一个泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。线程间数据隔离的,就意味着别的线程使用get()方法是拿不到其他线程set的值的。
set(T value)、get()、remove()三大主要方法的源码如下:
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取线程的ThreadLocalMap对象
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;
}
}
//没有就创建一个空的ThreadLocalMap对象
return setInitialValue();
}
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//没有就创建一个空的ThreadLocalMap对象
createMap(t, value);
}
public void remove() {
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* 获取线程的ThreadLocalMap对象
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
源码很简单,重点关注ThreadLocalMap类对象,ThreadLocalMap类是ThreadLocal类的内部类。ThreadLocalMap类对象要从线程Thread类中取到,是Thread类中名为threadLocals的成员变量。ThreadLocalMap类实际上是对自定义实现的Entry[]数组结构的封装,并非继承自原生Map类,Entry中的Key即是ThreadLocal变量本身,Value则是具体该线程中的变量副本值。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public class Thread implements Runnable {
//省略……
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//省略……
}
看到这里基本上可以找到ThreadLocal实现数据隔离的原理了,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量(Entry数组)里面的,别人没办法拿到,从而实现了隔离。
ThreadLocalMap底层结构
看源码可以发现,ThreadLocalMap并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表就是纯数组。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
//......
}
ThreadLocalMap数据结构大致如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/b8ab45e8309a6d6abbab5674c818dc09.png)
为什么要用数组,因为开发过程中一个线程可以有多个ThreadLocal来存放不同类型的对象,例如ThreadLocal<String>、ThreadLocal<Long>等,它们都放到当前线程的ThreadLocalMap里的,所以用数组来存。
多个Thread使用ThreadLocal维护本线程中变量的过程如下图所示,虚线箭头代表弱引用,实线箭头代表强引用。
ThreadLocalMap的弱引用及其引起的内存泄露
每个Thread内部都维护一个ThreadLocalMap字典数据结构,字典的Key值是ThreadLocal,value即为私有对象T。在Spring MVC中,经常用ThreadLocal保存当前登陆用户信息,这样线程在任意地方都可以取到用户信息了。仔细看源码,就会发现ThreadLocalMap中Entry的Key即ThreadLocal对象是采用弱引用引入的。
![](https://i-blog.csdnimg.cn/blog_migrate/57a87f9ded849ec44ead4be8654c4c4d.png)
ThreadLocal在保存变量的时候(即调用set方法存储一个变量时)会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
什么是弱引用?
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
怎么解决这个value值的内存泄露问题呢,在线程本地变量使用完成后,我们只要记得在使用的最后用remove把值清空就好了。
ThreadLocal<String> threadLocal = new ThreadLocal();
try {
threadLocal.set("abcde");
//……
String value = threadLocal.get();
//……
} catch (Exception e) {
e.printStack();
} finally {
threadLocal.remove();
}
为什么ThreadLocalMap对Entry中的key值ThreadLocal要设计成使用弱引用?
ThreadLocal的设计比较晦涩难懂,究其原因是我们通过threadLocal对象的set方法进行存储值,但数据并不是存储在ThreadLocal对象中,而是存储在当前调用该方法的线程对象中。但从应用者的角度来看,我们操作的对象是ThreadLocal,从设计上来说就应该为它考虑。考虑一个问题:如果应用程序觉得ThreadLocal对象的使命完成,将threadLocal的引用设置为null,如果Entry中的key值的引用类型是强引用的话,会发生什么问题?答案是:ThreadLocal对象会无法被垃圾回收器回收,因为从thread对象出发,有强引用指向threadlocal obj。此时会违背用户的初衷,造成所谓的内存泄露。
JVM发现某个线程(Thread对象)中的threadLocals成员变量存在着对某个ThreadLocal对象的强引用,就不会回收那个ThreadLocal对象。因此,使用弱引用可以防止长期存在的线程(通常使用了线程池)导致ThreadLocal无法回收造成内存泄漏。虽然Key值(ThreadLocal)是通过弱引用引入的,但是value即变量值本身是通过强引用引入。这就导致假如不作任何处理,由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry。
实际上,ThreadLocal内部已经为我们做了一定的防止内存泄漏的工作,即如下expungeStaleEntry()方法:
//staleSlot代表那样key=null的Entry在数组中的下标。
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--;
} 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;
}
上述方法的作用是擦除某个下标的Entry(置为null,可以回收),同时检测整个Entry[]表中对key为null的Entry一并擦除,重新调整索引。该方法在每次调用ThreadLocal的get、set、remove方法时都会执行,即ThreadLocal内部已经帮我们做了对key为null的Entry的清理工作。但是该工作是有触发条件的,需要调用相应方法,假如我们使用完之后不做任何处理是不会触发的。
ThreadLocal应用最佳实践
- 在ThreadLocal使用前后都调用remove清理,同时对异常情况也要在finally中清理
- ThreadLocal一般加static修饰,如果ThreadLocal不加static,则每次其所在类实例化时,都会有重复ThreadLocal创建。这样即使线程在访问时不出现错误也有资源浪费
那么我们在使用ThreadLocal时,set方法执行后没有执行remove方法就一定会造成内存泄露甚至内存溢出吗?
在实际情况中,其实不调用remove方法并不那么容易造成内存溢出,因为从存储结构来看,除非创建海量线程,并且这些线程都不释放,导致大量线程内部持有的ThreadLocalMap对象一直不会释放,但一个线程所持有的Entry对象个数的多少,取决于关联的ThreadLocal对象个数,故我们需要的关注点而不是remove方法,而是防止线程资源泄露。