ThreadLocal 源码简析

前言

今天看到推送的文章中有篇是关于的ThreadLocal,虽然之前有过了解,但是没有结合其应用场景,所以一直理解的不深刻。不深刻在于,我不理解为什么要为一个线程维护一个变量副本!

熟悉数据库连接的朋友们都知道为了提高使用效率会使用连接池,而为了线程安全的问题还要使用ThreadLocal。下面以JDBC中的connection做个简单解释。

ThreadLocal应用场景

先来看一个问题:j2ee的应用中,有一个用户请求就会启动一个线程。而如果我们把connection放在Threadlocal里的话,那么我们的程序只需要一个connection连接数据库就行了,每个线程都是用的connection的一个副本,那为什么还有必要要数据库连接池呢?

这个问题初看有点绕,我觉得是提这个问题的人理解错为ThreadLocal是专门用来提供connection的一个对象。为什么要用专门这个词因为它忽视了TheadLocal是给每个线程维护一个变量副本的事实。

假设有这么一个场景,用户下单的操作涉及到多个DAO,一个是库存的减和消费记录的加。这个过程要保证事务的特性的。为了要确保这多个特性就需要使用同一个connection。

1
2
库存Server.delet();
消费记录Server.add();

当执行第一步delet操作时,该方法会向连接池调用getConnection,然后在这个connection上执行减的操作。

当执行第二步add时,问题就来了。因为我们是向连接池getConnection,返回的connection本身就是一个随机对象,这就保证不了该connceiton与第一步是同一个对象。

所以这里就引入了TheadLocal。第一步执行的connection放入到TheadLocal中保存(上述代码中的注释(1)),第二步中的conncetion就从ThreadLocal中去拿即可。详细看下面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public Connection getCurrentConnecton(){  
Connection conn = threadLocal.get();
if(!isValid(conn)){ //如果ThreadLocal中有conn对象,就使用该对象,没有则调用getConnection
conn = getConnection();
}
return conn;
}

public synchronized Connection getConnection() {
Connection conn = null;
try {
if(contActive < this.dbBean.getMaxActiveConnections()){// 判断是否超过最大连接数限制
if (freeConnection.size() > 0) { //当前还有空闲的conn对象
conn = freeConnection.get(0);
if (conn != null) {
threadLocal.set(conn); //(1)
}
freeConnection.remove(0);
} else {//没有就new一个conn
conn = newConnection();
}
}else{
wait(this.dbBean.getConnTimeOut()); // 递归等待,直到获得连接
conn = getConnection();
}
if (isValid(conn)) {
activeConnection.add(conn);
contActive ++;
}
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return conn;
}

现在回头再来看刚刚提出的问题,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
22
public 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) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

ThreadLocal和Synchonized区别

Synchronized用于线程间的数据共享(使变量或代码块在某一时该只能被一个线程访问),是一种以延长访问时间来换取线程安全性的策略;

而ThreadLocal则用于线程间的数据隔离(为每一个线程都提供了变量的副本),是一种以空间来换取线程安全性的策略。

实际应用上理解为,当有多个DAO操作时,Synchronized给某一个connection加上锁,保证没有其他对象可以使用它。而ThreadLocal可以保证其他线程只会使用自己的connection。

ThreadLocal的内存泄漏

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

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使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

下面我们分两种情况讨论:

  1. key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  2. key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

比较两种情况,我们可以发现:

由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。