ThreadLocal详解
ThreadLocal简介
通常我们创建的一个变量可以被任意一个线程去修改,但是有时候想实现每个线程都有自己专属的本地变量不会被其它的线程去修改,这时候ThreadLocal应运而生。ThreadLocal类主要解决的问题就是让每个线程可以绑定自己专有的变量,相当于一个存放数据的盒子,每一个线程都有自己的盒子,每个线程只能访问到自己盒子中的变量,访问不到其它线程的盒子,即将变量私有化。
假如创建了一个ThreadLocal变量,那么每个线程在访问这个变量的时候相当于访问这个变量的本地副本,可以通过ThreadLocal中的get()方法去获取默认值,也可以通过set()方法去修改当前线程所持有的副本的值,这样就可以避免线程安全问题。
ThreadLocal样例
场景:在ThreadLocal中设置一个日期类并设置了日期的默认格式为YYYY-MM-dd HH:mm:ss
,创建多个线程去访问ThreadLocal中的日期格式的类型,然后去修改日期的格式为YYYY-MM-dd
,如果在其它的线程首次访问ThreadLocal中日期格式的时候,都是日期初始设置的格式,看不到其它线程修改后的日期格式,那么就可以说明ThreadLocal将变量私有化。
import java.text.SimpleDateFormat;
import java.util.Random;
public class ThreadLocalExample implements Runnable{
//创建ThreadLocal并设置了SimpleDateFormat为YYYY-MM-ddTHH:mm:ss
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"));
public static void main(String[] args) throws InterruptedException {
//创建一个Runnable实例对象,如果创建多个相当于多个ThreadLocal达不到测试效果,只能创建一个
ThreadLocalExample obj = new ThreadLocalExample();
for(int i=0 ; i<5; i++){
Thread t = new Thread(obj, ""+i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
@Override
public void run() {
//输出默认的日期格式
System.out.println("Thread Name= "+Thread.currentThread().getName()+" 默认格式 = "+formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改ThreadLocal中的日期格式
formatter.set(new SimpleDateFormat("YYYY-MM-dd"));
//输出修改后的日期格式
System.out.println("Thread Name= "+Thread.currentThread().getName()+" 当前格式 = "+formatter.get().toPattern());
}
}
运行结果如下:
可以看到每个线程第一次打印日期格式的时候,输出的都是初始设置的默认值,并没有访问到其它线程修改后的值。
ThreadLocal实现原理
首先我们可以看下Thread
的源码,在Thread中有两个变量threadLocals
和inheritableThreadLocal
,它们的类型都是ThreadLocalMap
类型的,ThreadLocalMap
是ThreadLocal
静态内部类,我们可以把 ThreadLocalMap理解为ThreadLocal类实现的定制化的HashMap,默认情况下这两个都是null,在调用ThreadLocal方法的set或者get方法的时候才会创建。
我们接着看一下ThreadLocal
的set方法,get方法和set方法原理类似,如下图所示:
通过上面的set方法代码可以看出,保存的本地变量最终保存到了当前线程的ThreadLocalMap中,并不是保存在了ThreadLocal中,ThreadLocal类可以通过Thread.currentThread()获取到当前线程,然后通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象,最后将值保存在当前线程的ThreadLocalMap中。ThreadLocalMap可以存储以ThreadLocal 为key的键值对。
ThreadLocal存在内存泄漏的问题
ThreadLocalMap中的key为ThreadLocal的弱引用,而value是强引用。所以,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap中就会出现key为null的Entry。如果我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。