一、介绍
根据 Java 官方文档的描述,我们可知 ThreadLocal 类用于提供线程内部的局部变量,其在多线程环境下能保证各个线程内部变量的隔离性。
换言之,ThreadLocal 提供线程内的局部变量,不同线程之间不会相互干扰,该变量作用范围贯穿线程的生命周期,减少同一线程内多个方法或组件之间一些公共变量传递的复杂度。
二、使用
2.1 常用方法
返回值 | 方法名 | 描述 |
---|
T | get() | 返回此线程局部变量的当前线程副本中的值 |
void | remove() | 移除此线程局部变量当前线程的值 |
void | set(T value) | 将此线程局部变量的当前线程副本中的值设置为指定值 |
2.2 案例演示
需求:用 3 名画家在一个画布上各自绘制一种颜色,并打印出其绘制的颜色。
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| /** * 画布类 */ public class Canvas { private String content;
public String getContent() { return content; }
public void setContent(String content) { this.content = content; } }
/** * 画家类 */ public class Painter extends Thread {
private String name; private Canvas canvas; private String color; public Painter(String name, Canvas canvas, String color) { this.name = name; this.canvas = canvas; this.color = color; }
@Override public void run() { canvas.setContent(color); System.out.println(this.name + "在画板绘制" + canvas.getContent()); } }
/** * 启动类 */ public class Demo {
public static void main(String[] args) { // 创建画布 Canvas canvas = new Canvas(); Painter painter1 = new Painter("小强", canvas, "红色"); Painter painter2 = new Painter("旺财", canvas, "黄色"); Painter painter3 = new Painter("狗蛋", canvas, "蓝色"); painter1.start(); painter2.start(); painter3.start(); } }
|
执行结果如下:
1 2 3
| 小强在画板绘制蓝色 旺财在画板绘制黄色 狗蛋在画板绘制黄色
|
显然,在多线程访问同一个资源(画布)的情况下,输出结果出现并发问题。
现有 2 种解决方案:一种是在 run 方法中加入 synchronized 同步代码块,另一种是使用 ThreadLocal 改造 Canvas 类型。
由于本篇着重介绍 ThreadLocal, 故下边我们通过第二种方式解决上述问题。
修改 Canvas 类为如下:
1 2 3 4 5 6 7 8 9 10 11 12
| public class Canvas { private ThreadLocal<String> map = new ThreadLocal();
public String getContent() { return map.get(); }
public void setContent(String content) { map.set(content); } }
|
启动执行类,运行结果如下:
1 2 3
| 小强在画板绘制红色 狗蛋在画板绘制蓝色 旺财在画板绘制黄色
|
结果正常输出。
2.3 ThreadLocal 与 synchronized 区别
名称 | 原理 | 侧重点 |
---|
ThreadLocal | 空间换时间,每个线程都都提供一份变量副本,从而实现同时访问而不相互干扰 | 多线程之间资源相互隔离 |
synchronized | 时间换空间,只提供一个变量,让线程排队访问 | 多线程之间共享资源,同步访问 |
三、ThreadLocal 内部结构
在看源码之前,我们可以试着猜测 ThreadLocal 内部结构是怎样的。
比如,ThreadLocal 内部定义了一个 Map 容器。当调用 ThreadLocal 实例的 set 方法时,以当前线程名/当前线程实例作为 key, 需要保存的内容作为 value 进行操作。当调用 get 方式时,以当前线程名/当前线程实例作为 key 获取数据。
上述方案看似可以正常实现功能,实则存在一些问题:
1 2 3
| 1) 由 ThreadLocal 维护 key-value 容器,当线程增多并调用 ThreadLocal 实例 的set 方法时,key-value 容器也随之增大,即内存占用也随之增大。
2) 当调用 ThreadLocal 实例方法的对象为线程池中的线程时,无法区分线程是否被循环使用,即当前线程之前已从线程池中被拿出调用 ThreadLocal 实例的 set 方法,如果当前调用 get 方法就会取出之前的数据造成数据污染等问题。
|
那么,ThreadLocal 内部到底是怎么实现线程间内部变量的隔离性的呢?
如上图,由 Thread 实例内部维护名为 ThreadLocalMap 的容器,其元素是以 ThreadLocal 实例为 key ,保存对象作为 value 的数据结构,与我们猜测的实现方式相反。
对比我们之前设想的方案,JDK 实现方案有 2 个好处:
1 2 3
| 1) Map 存储的 Entry 数量变少
2) 当线程销毁时,ThreadLocalMap 也随之销毁,减少内存使用
|
四、源码分析
4.1 ThreadLocal 源码
我们针对常用的 set、get、remove 方法进行源码剖析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public void set(T value) { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取当前线程对象维护的 ThreadLocalMap 对象 ThreadLocalMap map = getMap(t); if (map != null) // 如果 map 存在设置 entry map.set(this, value); else // 如果 map 不存在,由于 threadLocal 实例帮忙创建并绑定数据 createMap(t, value); }
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
|
set 方法执行流程:
1 2 3 4
| 1) 获取当前线程对象 2) 通过当前线程对象获取 ThreadLocalMap 对象 3) 如果 ThreadLocalMap 对象存在,则将入参设置进 ThreadLocalMap 对象中 4) 如果 ThreadLocalMap 对象不存在,则给当前线程创建 ThreadLocalMap 对象并设置入参
|
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
| public T get() { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取当前线程对象维护的 ThreadLocalMap 对象 ThreadLocalMap map = getMap(t); if (map != null) { // 如果 map 不为空,以当前的 ThreadLocal 实例为 key, 获取数据 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 如果 map 为空,初始化值,通常为 null return setInitialValue(); }
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
protected T initialValue() { return null; }
|
get 方法执行流程:
1 2 3 4
| 1) 获取当前线程对象 2) 通过当前线程对象获取 ThreadLocalMap 对象 3) 如果 ThreadLocalMap 对象存在,则以当前的 ThreadLocal 实例为 key, 获取数据 4) 如果 ThreadLocalMap 对象不存在,则通过 initialValue 方法初始化 value 值。
|
1 2 3 4 5
| public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
|
remove 方法执行流程:
1 2
| 1) 通过当前线程对象获取 ThreadLocalMap 对象 2) 如果 ThreadLocalMap 对象存在,则以当前的 ThreadLocal 实例为 key, 进行数据删除
|
4.2 ThreadLocalMap 源码
ThreadLocalMap 是 ThreadLocal 的内部类,其没有实现 Map 接口,单独实现了 Map 的功能。
成员变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| /** * 初始容量,必须是 2 的整次幂 */ private static final int INITIAL_CAPACITY = 16;
/** * 存放数据的 table,数据长度也是 2 的整次幂 */ private Entry[] table;
/** * 数组中 entry 的个数 */ private int size = 0;
/** * 进行扩展的阀值 */ private int threshold; // Default to 0
|
Entry 内部类:
1 2 3 4 5 6 7 8 9
| static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
|
Entry 继承 WeakReference 类,也就是 key 是弱引用,其目的是将 ThreadLocal 对象的生命周期与线程的生命周期解绑。
五、内存泄漏
虽然 ThreadLocal 作为弱引用 key 来使用,但是在某些情况下还是会造成内存泄漏问题。 在分析内存泄漏之前,我们先补充几个概念:
1 2 3 4 5 6 7
| 内存溢出:没有足够的内存供申请者使用
内存泄漏:程序中已动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,该问题最终会导致内存溢出
强引用:常见的对象引用,只要还有强引用指向一个对象,表明对象还“活着”,垃圾回收器就不会回收该对象
弱引用:垃圾回收期一旦发现只具有弱引用指向的对象,不管当前内存空间是否足够,都会回收该对象
|
了解了基本概念,接下来我们分析使用 ThreadLocal 出现内存泄漏的情况:
上图为一个线程使用 ThreacLocal 时的内存结构图,实线箭头表示强引用,虚线箭头表示弱引用。
1 2 3 4 5
| 当 ThreadLocal 使用结束,栈内存的 ThreadLocal 引用被回收,即引用 1 不再指向 ThreadLocal 对象。
由于引用 2 是弱引用,没有任何强引用指向 ThreadLocal 对象,因此 ThreadLocal 对象会被 GC 回收,此时 Entry 的 key = null
如果我们没有会手动删除 Entry 对象,且当前线程一直在运行中,会存在一个强引用链 Thread 引用-> Thread 对象-> ThreadLocal 对象-> Entry 对象 -> Value,由于 value 不会被回收,而 key 又为 null, value 这块内存就永远无法被访问,这就造成了内存泄漏,
|
既然使用弱引用作为 ThreadLocalMap 的 key 会造成内存泄漏,那为什么还要使用它呢?
其实,在 ThreadLocalMap 的 set、getEntry 方法中,会对 key 为 null 进行判断,如果为 null, 那么会将 value 也设置为 null。
换言之,在使用 ThreadLocal 的线程依然运行的情况下,我们忘记调用 remove 方法,弱引用比强引用多一层保障。弱引用指向的 ThreadLocal 对象被回收,对应的 value 在 TheadLocalMap 调用 set、getEntry、remove 任一方法时被设置为 null, 避免内存泄漏。
六、总结
1 2 3 4 5
| 适用于多线程并发场景
使用 ThreadLocal 在同一线程,不同组件中可传递公共变量
每个线程的变量都是相互独立,互不影响
|
注意:为防止内存泄漏,养成良好开发习惯,使用完 ThreadLocal 务必手动调用 remove 方法。