注意:文章案例使用了lambda表达式和线程池,阅读前请先对这两种技术做简单了解。
1、介绍
ThreadLocal是维持线程封闭性的一种规范的方法,这个类能使线程中的某个值与保存值得对象关联起来。每个Thread中都有一个ThreadLocalMap,使用ThreadLocal.set方法可以将该值的副本存在当前线程中,ThreadLocal.get方法将保存的值的副本取出。
每个Thread中都有一个ThreadLocalMap,当ThreadLocal.set方法被调用时,值其实是保存在了当前线程的ThreadLocalMap中,key值为ThreadLocal对象,value为当前值。
2、两种用途:
- 防止对可变的单实例对象或存在线程安全的全局变量进行共享;
- 将上下文保存在当前线程中,避免上下文信息层层传递。
3、案例分析
用途一:对于可变的单实例对象很容易理解,下面只演示对全局变量共享的用途。
我们先看一下错误案例。
代码1:
static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(() -> {
Date date = new Date(finalI * 1000);
System.out.println(simpleDateFormat.format(date));
});
}
executorService.shutdown();
}
如代码1所示,我们将SimpleDateFormat作为全局变量,每个线程都调用同一个SimpleDateFormat对象,因SimpleDateFormat并不是线程安全的,所以最终结果是错误的。
下面我们再看一下使用ThreadLocal的正确案例。
代码2:
static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int index= i;
executorService.execute(() -> {
Date date = new Date(index* 1000);
SimpleDateFormat simpleDateFormat = dateFormatThreadLocal.get();
System.out.println(simpleDateFormat.format(date));
});
}
executorService.shutdown();
}
如代码2所示,我们在声明ThreadLocal时,给它赋一个初始值,我们可以通过get方法获得这个初始值的副本,不同的子线程将获得不同的副本,线程之间不会影响,就不会出现线程安全问题。
用途二:将上下文保存在当前线程中,避免上下文信息层层传递。
我们演示一下这种场景。
代码3:
static ThreadLocal<Date> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 2; i++) {
executorService.execute(() -> {
Date date = new Date();
System.out.println(Thread.currentThread().getName() + " set的Date的hashcode:" + System.identityHashCode(date));
threadLocal.set(date);
test1();
threadLocal.remove();
});
}
executorService.shutdown();
}
static void test1() {
test2();
}
static void test2() {
test3();
}
static void test3() {
Date date = threadLocal.get();
System.out.println(Thread.currentThread().getName() + " get的Date的hashcode:"+ System.identityHashCode(date));
}
如代码3所示,我们使用线程池创建两个线程,并且在子线程的run方法中声明一个Date对象并调用ThreadLocal的set方法,在test3方法中调用get方法获得Date对象。
我们看看运行结果:
结果4:
pool-1-thread-1 set的Date的hashcode:1070855846
pool-1-thread-2 set的Date的hashcode:1683005194
pool-1-thread-1 get的Date的hashcode:1070855846
pool-1-thread-2 get的Date的hashcode:1683005194
我们通过对比set和get的Date对象的hashcode值,确定是否是同一个Date对象。我们看到子线程pool-1-thread-1和pool-1-thread-2两处的Date对象的hashcode值均相同,验证了用途二并且这种方法是线程安全的。
4、源码分析
下面我们对以上演示进行源码分析,我们首先看一下ThreadLocal的set方法的源码。
源码5:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
从源码5中看到,进入set方法首先获得当前线程对象,调用getMap方法获得当前对象的ThreadLocalMap,如果ThreadLocalMap为null,就会创建该对象。得到ThreadLocalMap后,将set传入的值保存在Map中,key为当前ThreadLocal对象。在代码3中,即使ThreadLocal为全局变量也不会出现线程安全问题,就是因为set的值并不会保存在ThreadLocal中,而是保存在当前线程中的ThreadLocalMap中。
下面我们看一下withInitial方法的源码。
源码6:
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
我们从源码6中看到,通过withInitial传入的匿名内部类的对象,保存在ThreadLocal的子类SuppliedThreadLocal中。
下面我们看一下get方法的源码,是怎么从获取值得。
源码7:
public T get() {
Thread t = Thread.currentThread();
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();
}
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;
}
@Override
protected T initialValue() {
return supplier.get();
}
从源码7中看到,调用进入get方法后,首先获得map中的值并返回,我们从源码5中了解到,调用set方法,将值保存到map中,这里对应用途2中的代码3。如果map为null或者map中的值为null,代码则调用setInitialValue方法,进入该方法,第一行又要调用initialValue方法,我们进入子类SuppliedThreadLocal的initialValue,发现该方法获取我们通过ThreadLocal.withInitial方法传入的匿名类对象中的返回值。在setInitialValue方法中得到该值并保存到当前线程的ThreadLocalMap中并返回该值。这儿是怎么实现线程安全的呢?其实通过ThreadLocal.withInitial传入的匿名内部类是Supplier实现,在initialValue方法中每次调用一次supplier.get(),就会执行一次withInitial中的方法体,相对于代码2,就会创建一个SimpleDateFormat对象并返回,所以不同的线程获得的是不同的SimpleDateFormat对象,保存在ThreadLocalMap中的值也是不同的,也就是不同的SimpleDateFormat副本。
5、缺点
ThreadLocal有其强大的地方,但并不是完美的,下面我们说说它的缺点。
缺点1:可能引起资源浪费
我们在代码3中可以发现有一行是threadLocal.remove();字面意思就是删除threadLocal.set保存在值。我们知道在线程池中子线程是循环利用的,因为强引用的关系,threadLocal.set的值并不会因为run方法的执行结果而被回收,所以需要调用remove方法,将值从ThreadLocalMap中删掉,这样就会被垃圾回收期回收。
缺点2:降低代码可重用性,同时引入类之间隐含的耦合性
我们看一下代码3,如果我们想只重用test3方法,threadLocal.get的返回值是null,再严重些回到之空指针异常,因此降低了test3方法的重用性。为了避免这种错误,我们不得不获得threadLocal的对象并set一个Date对象,这样就导致与test3所在的类进行了耦合。
6、总结
ThreadLocal有其强大的地方,应当根据实际的业务需求适当使用。因有其缺点,拒绝滥用,否则得不偿失的。