一、简介
首先ThreadLocal 是一个线程的局部变量(其实就是一个Map),ThreadLocal会为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,将对象的可见范围限制在同一个线程内,而不会影响其它线程所对应的副本。
这样做其实就是以空间换时间的方式(与synchronized相反),以耗费内存为代价,但大大减少了线程同步(如synchronized)所带来性能消耗以及减少了线程并发控制的复杂度。
public class Demo2 {
static String str;
public void setStr(String str) {
this.str = str;
}
public static String getStr() {
return str;
}
public static void main(String[] args) {
Demo2 demo2 = new Demo2();
for (int i = 0; i < 5; i++) {
new Thread(()->{
demo2.setStr(Thread.currentThread().getName()+"的数据");
System.out.println("----------------");
System.out.println(Thread.currentThread().getName()+"获取-----"+demo2.getStr());
},"线程"+i).start();
}
}
}
需求:线程隔离,各自线程输出各自的数据
多运行几遍,可能会得到下面的结果:
解决方法:使用ThreadLocal。
共享变量一直是并发中的老大难问题,每个线程都对它有操作权,所以线程之间的同步很关键,锁也就应运而生。这里换一个思路,是否可以把共享变量私有化?即每个线程都拥有一份共享变量的本地副本,每个线程对应一个副本,同时对共享变量的操作也改为对属于自己的副本的操作,这样每个线程处理自己的本地变量,形成数据隔离。事实上这就是ThreadLocal了。
public class Demo2 {
private ThreadLocal<String> t = new ThreadLocal<>();
public void setStr(String str) {
t.set(str);
}
public String getStr() {
return t.get();
}
public static void main(String[] args) {
Demo2 demo2 = new Demo2();
for (int i = 0; i < 5; i++) {
new Thread(()->{
demo2.setStr(Thread.currentThread().getName()+"的数据");
System.out.println("----------------");
System.out.println(Thread.currentThread().getName()+"获取-----"+demo2.getStr());
},"线程"+i).start();
}
}
}
二、ThreadLocal内部结构
在JDK7中
每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样能达到各个线程之间的局部变量相互隔离。
在JDK8以后
每个Thread线程内部都有一个ThreadLocalMap,这个Map的key就是ThreadLocal实例本身,value才是真正存储的值
具体过程:
- 每个Thread线程内部都有一个Map(ThreadLocalMap)
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责从Map获取和设置线程变量值
- 对于不同的线程,每次获得副本时,别的线程并不能获得当前线程副本值,形成了副本的隔离,互不干扰
JDK8的设计方案两个好处
1.每个Map存储的Entry数量变少。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际开发中,ThreadLocal的数量往往少于线程的数量。数量变少也可以尽量避免哈希冲突。
2.当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用。
三、ThreadLocal 内存泄漏
ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,那么 ThreadLocalMap 中保存的 key 值就变成了 null,而 Entry 又被 threadLocalMap 对象引用,threadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不终结的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。
简单说:就是ThreadLocalMap中的key是弱引用,发生GC时会被回收,而value一直在啊内存中没有被回收,造成内存泄漏
即使key使用弱引用,也不能完全保证不会内存泄漏
那么如何避免内存泄漏呢?
在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收,其中 remove 源码如下所示: