PPXu

总结ThreadLocal

2018-11-05

ThreadLoacal简介

ThreadLocal类是修饰变量的,重点是在控制变量的作用域,初衷可不是为了解决线程并发和线程冲突的,而是为了让变量的种类变的更多更丰富,方便人们使用罢了。


根据变量的作用域,可以将变量分为全局变量,局部变量。简单的说,类里面定义的变量是全局变量,函数里面定义的变量是局部变量。
还有一种作用域是线程作用域,线程一般是跨越几个函数的。为了在几个函数之间共用一个变量,所以才出现:线程变量,这种变量在Java中就是ThreadLocal变量。


ThreadLocal变量,不同于它们的普通对应物,因为访问某个变量(通过其get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。所以我们说,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。

ThreadLocal的用处

Web开发中常见到的一个问题:多用户session问题。
假设有多个用户需要获取用户信息,一个线程对应一个用户。在mybatis中,session用于操作数据库,那么设置、获取操作分别是session.set()、session.get(),如何保证每个线程都能正确操作达到想要的结果呢?

假如我们要设置一个变量,作为各个线程共享的变量,来存储session信息,那么当我们需要让每个线程独立地设置session信息而不被其它线程打扰,要怎么做呢?很容易想到了加锁,譬如synchronized,互斥同步锁synchronized自JDK1.5经过优化后,不会很消耗资源了,但当成千上万个操作来临之时,扛高并发能力不说,数据返回延迟带来的用户体验变差又如何解决?

那么,就上文提出的问题,引申出来,像mybatis,hibernate一类的框架是如何解决这个session问题的呢?

来看一下,mybatis的SqlSessionManager类:

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
39
40
41
42
public class SqlSessionManager implements SqlSessionFactory, SqlSession {

private final SqlSessionFactory sqlSessionFactory;
private final SqlSession sqlSessionProxy;

private ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<SqlSession>();

private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[]{SqlSession.class},
new SqlSessionInterceptor());
}

...

public void startManagedSession() {
this.localSqlSession.set(openSession());
}

public void startManagedSession(boolean autoCommit) {
this.localSqlSession.set(openSession(autoCommit));
}

public void startManagedSession(Connection connection) {
this.localSqlSession.set(openSession(connection));
}

...

@Override
public Connection getConnection() {
final SqlSession sqlSession = localSqlSession.get();
if (sqlSession == null) {
throw new SqlSessionException("Error: Cannot get connection. No managed session is started.");
}
return sqlSession.getConnection();
}

...
}

留意到,mybatis里的localSqlSession就是用的ThreadLocal变量来实现。

从内存模型出发看ThreadLocal:

我们知道,在虚拟机中,堆内存就是用于存储共享数据,也就是这里所说的主内存。

每个线程将会在堆内存中开辟一块空间叫做线程的工作内存,附带一块缓存区用于存储共享数据副本。那么,共享数据在堆内存当中,线程通信就是通过主内存为中介,线程在本地内存读并且操作完共享变量操作完毕以后,把值写入主内存。

  1. ThreadLocal被称为线程局部变量,说白了就是线程工作内存的一小块内存,用于存储数据。
  2. 那么,ThreadLocal.set()、ThreadLocal.get()方法,就相当于把数据存储于线程本地,取也是在本地内存读取。就不会像synchronized需要频繁的修改主内存的数据,再把数据复制到工作内存,也大大提高访问效率。

那么,我们再来回答上面引出的问题,mybatis为什么要用ThreadLocal来存储session?

首先,因为线程间的数据交互是通过工作内存与主存的频繁读写完成通信,然而存储于线程本地内存,提高访问效率,避免线程阻塞造成cpu吞吐率下降。再者,在多线程中,每一个线程都各自维护session,轻易完成对线程独享资源的操作。

理解ThreadLocal的关键源码

首先,要理解ThreadLocal的数据结构,我们可以看它的set/get方法:
ThreadLocal.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

...

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

...

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

Thread.java

1
ThreadLocal.ThreadLocalMap threadLocals = null;
  1. ThreadLocalMap作为ThreadLocal的静态内部类,用于存储多个ThreadLocal对象
  2. ThreadLocal对象作为ThreadLocalMap的key来存储,我们set进去的独享数据作为value存储
  3. 留意到它里边调到的getMap(Thread)方法,得知ThreadLocalMap的获取跟当前Thread有关,仔细看threadLocals其实就是当前线程的一个ThreadLocalMap变量。也就是说,一个线程对应一个ThreadLocalMap,get()就是当前程获取自己的ThreadLocalMap。
1
2
3
4
5
6
7
8
9
10
11
12
13
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对象作为key,去获取存储于ThreadLocalMap中的值。

ThreadLocal内存泄露

引用关系图

先引用一张经典的引用关系图来说明当前线程(currentThread)以及threadLocalMap、key、threadLocal实例几个之间的引用关系:

利用这图来回顾总结一下ThreadLocal的实现:
每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。

也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

ThreadLocal为什么会内存泄漏

我们可以理解到,每个线程都会创建一块工作内存,每个线程都有一个ThreadLocalMap,而ThreadLocalMap可以有多个key,也就是说可以存储多个ThreadLocal。那么假设,开启1万个线程,每个线程创建1万个ThreadLocal,也就是每个线程维护1万个ThreadLocal小内存空间!
那么,当线程执行结束以后,如果一个ThreadLocal没有外部强引用来引用它而是用弱引用来引用,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

Key使用什么引用才好?

如上,key对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就会导致内存泄漏,而不是因为弱引用。

ThreadLocal防内存泄露最佳实践

综上,我们可以理解ThreadLocal为避免内存泄露的设计大致上是:

  1. JVM利用ThreadLocalMap的Key为弱引用,来避免ThreadLocal内存泄露。
  2. 由于Key设置为弱引用,那么,当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
    所以,每次使用完ThreadLocal,都调用它的remove()方法,清除数据,则可以达到回收弱引用的结果,这是最佳的使用实践。否则,在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

Reference

《并发编程(四):ThreadLocal从源码分析总结到内存泄漏》
《深入分析 ThreadLocal 内存泄漏问题》

扫描二维码,分享此文章