前言
今天看到推送的文章中有篇是关于的ThreadLocal
,虽然之前有过了解,但是没有结合其应用场景,所以一直理解的不深刻。不深刻在于,我不理解为什么要为一个线程维护一个变量副本!
熟悉数据库连接的朋友们都知道为了提高使用效率会使用连接池,而为了线程安全的问题还要使用ThreadLocal。下面以JDBC中的connection做个简单解释。
ThreadLocal应用场景
先来看一个问题:j2ee的应用中,有一个用户请求就会启动一个线程。而如果我们把connection放在Threadlocal里的话,那么我们的程序只需要一个connection连接数据库就行了,每个线程都是用的connection的一个副本,那为什么还有必要要数据库连接池呢?
这个问题初看有点绕,我觉得是提这个问题的人理解错为ThreadLocal是专门
用来提供connection的一个对象。为什么要用专门这个词因为它忽视了TheadLocal是给每个线程维护一个变量副本的事实。
假设有这么一个场景,用户下单的操作涉及到多个DAO,一个是库存的减和消费记录的加。这个过程要保证事务的特性的。为了要确保这多个特性就需要使用同一个connection。
1 | 库存Server.delet(); |
当执行第一步delet操作时,该方法会向连接池调用getConnection,然后在这个connection上执行减的操作。
当执行第二步add时,问题就来了。因为我们是向连接池getConnection,返回的connection本身就是一个随机对象,这就保证不了该connceiton与第一步是同一个对象。
所以这里就引入了TheadLocal。第一步执行的connection放入到TheadLocal中保存(上述代码中的注释(1)),第二步中的conncetion就从ThreadLocal中去拿即可。详细看下面代码。
1 | public Connection getCurrentConnecton(){ |
现在回头再来看刚刚提出的问题,Threadlocal中虽然有conn的副本对象,但是这个对象实际上是由连接池给的,而TreadLocal是为了维护一个线程使用同一个对象而存在的。使用两者不存在有我没他的关系。
ThreadLocal源码简析
看一下ThreadLocal的set方法
1 | threadLocal.set(conn); |
乍一看ThreadLocal的使用方法,会误以为ThreadLocal是个map。的确没在看源码之前,我以为ThreadLocal存储方式是以Thread.currentThread()为key,conn为value存储的。但这个想法是错的。假设ThreadLocal真是这么做的,你会发现每个线程都只能存储一个局部变量,太不实用了。
实际原理是:Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
经过上面的解释,再来看set方法就会非常的明朗,显示获得当前线程的ThreadLocalMap,然后以threadLocal为key,存储的值为value进行保存。get的方法也是类似的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
"unchecked") (
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal和Synchonized区别
Synchronized用于线程间的数据共享(使变量或代码块在某一时该只能被一个线程访问),是一种以延长访问时间来换取线程安全性的策略;
而ThreadLocal则用于线程间的数据隔离(为每一个线程都提供了变量的副本),是一种以空间来换取线程安全性的策略。
实际应用上理解为,当有多个DAO操作时,Synchronized给某一个connection加上锁,保证没有其他对象可以使用它。而ThreadLocal可以保证其他线程只会使用自己的connection。
ThreadLocal的内存泄漏
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收。
这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
下面我们分两种情况讨论:
- 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就会导致内存泄漏,而不是因为弱引用。