0%

深入浅出Java多线程-ThreadLocal

ThreadLocal 用来解决什么

如果一段代码中所需要的数据必须与其他代码共享,并且共享数据的代码能保证在同一个线程中执行,那我们就可以通过ThreadLocal 来把共享数据的可见范围限制在同一个线程之内,这样的好处是,不需要同步也能保证线程之间不出现数据争用问题。

变量既需要在方法或类之间共享,又期望在线程间隔离的场景,就非常适合使用 ThreadLocal 来实现。

常见场景:

  • 大部分使用消息队列的模式,如 “生产者-消费者“模式,都会将消息的消费过程尽量在一个线程中消费完,比如在经典Web交互模式中“一个请求对应一个服务器线程”的这种方式
  • 还有像线程内上线文管理器、数据库连接等可以用到 ThreadLocal
  • 比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。如果单纯的理解一个用户的一次会话对应服务端一个独立的处理线程,那用 ThreadLocal 在存储 Session ,简直是再合适不过了。但是例如 tomcat 这类的服务器软件都是采用了线程池技术的,并不是严格意义上的一个会话对应一个线程。并不是说这种情况就不适合 ThreadLocal 了,而是要在每次请求进来时先清理掉之前的 Session ,一般可以用拦截器、过滤器来实现。

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
39
40
41
42
public class ThreadlocalDemo {

/**
* 创建 ThreadLocal对象
*/
private static ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>();

/**
* 创建并赋初值 (通过initialValue方法)
*/
private static ThreadLocal<String> mLocal = ThreadLocal.withInitial(() -> "init value");

public static void main(String[] args) {

try {
mThreadLocal.set(true);
mLocal.set(Thread.currentThread().getName() + "A");
System.out.println(Thread.currentThread().getName() + "# " + mThreadLocal.get() + "#" + mLocal.get());

new Thread("Thread#1") {
@Override
public void run() {
mThreadLocal.set(false);
mLocal.set(this.getName() + "B");
System.out.println(this.getName() + "# " + mThreadLocal.get() + "#" + mLocal.get());
}
}.start();

new Thread("Thread#2") {
@Override
public void run() {
mLocal.set(this.getName() + "C");
System.out.println(this.getName() + "# " + mThreadLocal.get() + mLocal.get());

}
}.start();
} finally {
mThreadLocal.remove();
mLocal.remove();
}
}
}

b6196d00cfc59354331d1d8331d24073.png

ThreadLocal 的原理

每一个线程的 Thread 对象中都有一个 自己的 ThreadLocalMap 对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值的 K-V 键值对,ThreadLocal 对象就是当前线程的 ThreadLocalMap 的访问入口,每一个 ThreadLocal 对象都包含了一个独一无二的threadLocalHashCode 值, 使用这个值就可以在线程 K-V 键值对 中查找对应的本地线程变量。(ThreadLocalMap 由 Thread 维护,从而使得每个Thread 只能访问自己的 Map, 所以不存在数据争用问题)

ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的 Thread 中有不同的副本(实际是不同的实例,后文会详细阐述)。这里有几点需要注意:

  • 每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题,所以也不需要同步的措施

下面的图片来源于 http://www.jasongj.com/java/threadlocal/
e32f66e757201462c069951ee91ca32d.png

ThreadLocal 的源码分析

ThreadLocal 的主要方法是:get()、set()、remove()

Thread 与 ThreadLocal 与 ThreadLocalMap 的结构关系

Thread 内部维护了一个 ThreadLocalMap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
//省略其他代码
}

ThreadLocalMap是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
public class ThreadLocal<T> {
//省略其他代码

static class ThreadLocalMap {
// Entry类继承了WeakReference<ThreadLocal<?>>,即每个Entry对象都有一个ThreadLocal的弱引用
//(作为key),这是为了防止内存泄露。一旦线程结束,key变为一个不可达的对象,这个Entry就可以被GC了。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// ThreadLocalMap 的初始容量,必须为2的倍数
private static final int INITIAL_CAPACITY = 16;

// resized时候需要的table
private Entry[] table;

// table中的entry个数
private int size = 0;

// 扩容数值
private int threshold; // Default to 0
}
}

ThreadLocal#set

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个 ThreadLocalMap 类对应的 get()、set() 方法。例如下面的 set 方法:

1
2
3
4
5
6
7
8
9
10
11
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 根据当前线程的对象获取其内部Map
ThreadLocalMap map = getMap(t);
// 注释1
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

调用 ThreadLocal 的 set 方法时,首先获取到了当前线程,然后获取当前线程维护的 ThreadLocalMap 对象,最后在 ThreadLocalMap 实例中添加上。如果 ThreadLocalMap 实例不存在则初始化并赋初始值。

这里看到 set 方法的第一个参数是 this ,this即指的是当前的 ThreadLocal 对象,会看上看的代码就是指的 mLocal 这个对象。而在 ThreadLocalMap 的 set 方法中会根据当前 ThreadLocal 对象实例,做一些操作和判断,最终实现赋值操作(具体参考源码)。

ThreadLocal#get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public T get() {
// 获取Thread对象t
Thread t = Thread.currentThread();
// 获取t中的map
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();
}

所以说,最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是一个中间工具,传递了变量值。

ThreadLocal 使用的时候需要特别的内存泄漏问题

内存泄漏问题分析

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

源码如下:

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
public class ThreadLocal<T> {
//省略其他代码

static class ThreadLocalMap {

// Entry类继承了WeakReference<ThreadLocal<?>>,即每个Entry对象都有一个ThreadLocal的弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// ThreadLocalMap 的初始容量,必须为2的倍数
private static final int INITIAL_CAPACITY = 16;

// resized时候需要的table
private Entry[] table;

// table中的entry个数
private int size = 0;

// 扩容数值
private int threshold; // Default to 0
}
}

ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。所以在使用完 ThreadLocal 之后一定要手动调用 remove() 方法。不然极有可能会导致内存泄漏。

典型使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
//实例通常总是以静态字段初始化如下
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

void processUser(user) {
try {
threadLocalUser.set(user);
step1();
step2();
} finally {
threadLocalUser.remove();
}
}

总结强调

  • 使用完 ThreadLocal ,最好手动调用 remove() 方法,例如上面说到的 Session 的例子,如果不在拦截器或过滤器中处理,不仅可能出现内存泄漏问题,而且会影响业务逻辑;
  • 使用 ThreadLocal 的时候,最好要声明为静态的;

参考资料

1.正确理解Thread Local的原理与适用场景
2.ThreadLocal 原理和使用场景分析