ThreadLocal的应用及原理,这些变量与普通变量的
ThreadLocal的应用及原理,这些变量与普通变量的
1. ThreadLocal 是什么
JDK 对ThreadLocal
的描述为:
此类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问一个变量的线程(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,这些字段希望将状态与线程(例如,用户ID或事务ID)相关联。
说白了,ThreadLocal
就是用来存放线程自身相关数据的一个容器,这个容器叫做ThreadLocalMap
,它是ThreadLocal
的一个静态内部类,同时作为Thread
类的一个成员变量。ThreadLocal
在使用时,先拿到当前线程的成员变量ThreadLocalMap
,以当前的ThreadLocal
对象作为key
,变量作为value
存入ThreadLocalMap
。 然后每个线程取变量都是从线程各自的ThreadLocalMap
中取值,自然是线程安全的了。因为变量只在自己线程的生命周期内起作用,所以说ThreadLocal
提供线程局部变量,或者叫线程本地变量。
ThreadLocal 的特点有3个:
- 线程并发:在多线程并发的场景下使用。
- 数据传递:通过 ThreadLocal ,在同一个线程中,不同组件中传递公共变量。
- 线程隔离:不同线程之间互不干扰,这种变量在线程的生命周期内起作用。
2. ThreadLocal 怎么用
ThreadLocal 的常用方法有:
public ThreadLocal()
:通过构造器创建对象。一般是静态的。<S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
:初始化一个 ThreadLcoal。void set(T value)
:设置当前线程绑定的局部变量。T get()
:获取当前线程绑定的局部变量。void remove()
:删除当前线程绑定的局部变量。
2.1 使用入门
2.1.1 原始版本
现在模拟一个需求,一个线程在业务开始时初始化一个用户 id(类似在一次web请求中上下文中初始化一下用户信息),业务结束时获取这个用户 id(比如用来打印日志,或者作为一个公共变量运用到业务编码中),存在多个这样的线程。
public class ThreadLocalTest {
private String userId;
private String getUserId() {
return userId;
}
private void setUserId(String userId) {
this.userId = userId;
}
public static void main(String[] args) {
ThreadLocalTest test = new ThreadLocalTest();
for (int i = 1; i < 6; i++) {
Thread thread = new Thread(() -> {
// 当前线程初始化userId
test.setUserId(Thread.currentThread().getName() + "的userId");
// 执行其他业务代码
System.out.println("===执行业务代码===");
// 当前线程获取userId
System.out.println(Thread.currentThread().getName() + "-->" + test.getUserId());
});
thread.setName("线程" + i);
thread.start();
}
}
}
一种可能的结果:
===执行业务代码===
线程2-->线程1的userId
===执行业务代码===
线程1-->线程3的userId
===执行业务代码===
线程3-->线程3的userId
===执行业务代码===
线程4-->线程4的userId
由于线程调度的不确定性,可能线程1运行到一半,切换到了线程2,于是线程2获取到的 userId 是线程1设置的。也就是说,每个线程之间的变量不是隔离的,造成数据错误。
2.1.2 ThreadLocal 版本
每个线程中的变量都存放到自己的线程当中,所以这些变量叫做线程局部变量很形象。
public class ThreadLocalTest {
private static ThreadLocal<String> context = new ThreadLocal<>();
private String getUserId() {
return context.get();
}
private void setUserId(String userId) {
context.set(userId);
}
public static void main(String[] args) {
ThreadLocalTest test = new ThreadLocalTest();
for (int i = 1; i < 5; i++) {
Thread thread = new Thread(() -> {
test.setUserId(Thread.currentThread().getName() + "的userId");
System.out.println("===执行业务代码===");
System.out.println(Thread.currentThread().getName() + "-->" + test.getUserId());
context.remove(); // 使用完清理线程局部变量
});
thread.setName("线程" + i);
thread.start();
}
}
}
这样每个线程就互不干扰,不会取错变量值。一种可能的结果如下:
===执行业务代码===
线程1-->线程1的userId
===执行业务代码===
线程4-->线程4的userId
===执行业务代码===
线程2-->线程2的userId
===执行业务代码===
线程3-->线程3的userId
2.1.3 synchronized 版本
如果只看结果的正确性,用 synchronized 给业务代码块加锁也是可以完成的。如下:
Thread thread = new Thread(() -> {
synchronized (ThreadLocalTest.class) {
test.setUserId(Thread.currentThread().getName() + "的userId");
System.out.println("===执行业务代码===");
System.out.println(Thread.currentThread().getName() + "->" + test.getUserId());
}
});
这样完全可以实现需求,但是 synchronized 的问题是什么呢?我们总说谁谁谁是线程安全的类,因为它有 synchronized 修饰。就是因为 synchronized 让多线程变成了单线程,它一次只允许一个线程执行,它能不安全吗?但它带来的代价是性能的下降,它不能并发执行,而 ThreadLocal 可以并发执行。
2.1.4 ThreadLocal 和 synchronized 对比
综上,synchronized 和 ThreadLocal 两个处理问题的角度和场景是不同的。
- synchronized 的侧重点在于保证操作的原子性,保证并发场景下共享变量的数据一致性。
- ThreadLocal 强调线程隔离性,不同的线程互不干扰,保证并发场景下数据传递的正确性。在web请求上下文中较为常见。
3. ThreadLocal 原理
3.1 代码结构
ThreadLocal 的原理要从它的set(T value)
、get()
方法的源码入手。在 set 值的时候,首先会获取当前线程一个的成员变量ThreadLocalMap
,ThreadLocalMap
的 key 是当前ThreadLocal
对象,value 是要存入的值。这个 key 和 value 会存到哪里呢?ThreadLocalMap
还有个内部类Entry
,这个Entry
继承了WeakReference
,key 赋值给弱引用,也就是当前的ThreadLocal
对象,value 则赋值给Entry的成员变量value
。ThreadLocalMap
也是一个哈希表(所谓哈希表,也叫散列表,它基于数组,通过某种哈希算法计算出一系列关键字对应的散列值,然后以这些散列值作为数组索引将数据存放到对应位置,达到快速查找的目的),它内部维护一个Entry
数组,来存储键值对。存数据的时候也是通过哈希函数计算ThreadLocal 对象对应的数组下标,然后放入Entry
数组中。
3.2 内存泄漏问题
ThreadLocal 会发生内存泄漏吗?我们结合代码慢慢分析。
在 2.1.1 节中有这样的代码:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private void setUserId(String userId) {
threadLocal.set(userId);
}
// ...
}
首先,我们new
了一个 ThreadLocal 对象,这里存在一个强引用:threadLocal
引用变量指向 ThreadLocal 对象。其次,当其他线程执行setUserId
方法时,ThreadLocal 的set
方法最终是把数据存到了ThreadLocalMap
中的Entry
,看源码我们会发现,存数据最终是调用Entry
的构造器Entry(ThreadLocal<?> k, Object v)
完成的,而k
这个参数是传入的this
对象,说明什么?我们使用 ThreadLocal 对象调用set
,那this
肯定是当前new
出来的 ThreadLocal 对象!再次说明,我们new
出来的 ThreadLocal 对象有两个引用指向它:
threadLocal
变量的强引用。- 在
Entry
中的弱引用。
此时再看一张图(这张图被广泛引用,感谢原图作者
用户点评