一、ThreadLocal解决了什么问题
网上有很多关于ThreadLocal解决了什么问题的资料,但是很多都是不正确的。
不正确的理解
- ThreadLocal为解决多线程程序的并发问题提供了一种新的思路
- ThreadLocal的目的是为了解决多线程访问资源时的共享问题
正确的理解
看看JDK中的源码是怎么写的:
【原文】This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
【翻译】ThreadLocal类提了供线程本地变量。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的副本。ThreadLocal 变量通常被private static修饰,用于关联线程上下文。
ThreadLoal变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal<String>而言即为 String 类型变量),在不同的 Thread 中有不同的副本。
因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。那就不存在多线程间共享资源的问题,既无共享,何来同步问题,又何来解决同步问题一说?
通过上面的分析,可以一句话总结ThreadLocal解决的问题:ThreadLocal提供了线程本地变量,每个线程都有一个该变量的副本,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
二、ThreadLocal应用场景
基于ThreadLocal解决的问题,我们可以将ThreadLocal应用到很多场景中。
- 场景一:当一个变量需要在线程间隔离而在方法或类间共享时,可以使用ThreadLocal。
- 场景二:……
三、ThreadLocal用法
public class ThreadLocalDemo {
public static void main(String[] args) {
new Thread(new InnerClass(),"Thread-1").start();
new Thread(new InnerClass(),"Thread-2").start();
new Thread(new InnerClass(),"Thread-3").start();
}
private static class InnerClass implements Runnable{
@Override
public void run() {
Counter.count.set(Counter.count.get()+1);
System.out.println(Thread.currentThread().getName()+
",count-hashcode="+Counter.count.hashCode()+
",count-value="+Counter.count.get());
}
}
private static class Counter{
public static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
}
}
//执行结果
Thread-2,count-hashcode=742865302,count-value=1
Thread-3,count-hashcode=742865302,count-value=1
Thread-1,count-hashcode=742865302,count-value=1
从上面的输出可以看出,每个线程的count对象是不一样。
四、ThreadLocal原理
方案一:ThreadLocal维护线程与实例之间的映射
既然每个访问 ThreadLocal 变量的线程都有自己的一个“本地”实例副本。一个可能的方案就是 ThreadLocal 维护一个 Map,键是 Thread,值是它在该 Thread 内的实例。线程通过该 ThreadLocal 的 get() 方案获取实例时,只需要以线程为键,从 Map 中找出对应的实例即可。该方案如下图所示
该方案可满足上文提到的每个线程内一个独立备份的要求。每个新线程访问该 ThreadLocal 时,需要向 Map 中添加一个映射,而每个线程结束时,应该清除该映射。这里就有两个问题:
- 增加线程与减少线程均需要写 Map,故需保证该 Map线程安全。虽然从ConcurrentHashMap的演进看Java多线程核心技术一文介绍了几种实现线程安全 Map的方式,但它或多或少都需要锁来保证线程的安全性
- 线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏(后文会介绍避免内存泄漏的方法)
其中锁的问题,是 JDK 未采用该方案的一个原因。
方案二:Thread维护ThreadLocal与实例的映射
上述方案中,出现锁的问题,原因在于多线程访问同一个 Map。如果该 Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。该方案如下图所示。
该方案虽然没有锁的问题,但是由于每个线程访问某 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),则这些 ThreadLocal 不能被回收,可能会造成内存泄漏。后文会介绍 JDK 如何解决该问题。
下面从源码的角度来看看具体实现。
set方法:
public void set(T value) {
Thread t = Thread.currentThread();
//获取当前线程维护的map。
//Thread内部维护了一个ThreadLocal.ThreadLocalMap类型的变量,这个变量存储的就是线程本地变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//创建map
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
//ThreadLocal变量在map的Entry数组中的位置,是由threadLocal对象的hashCode决定的
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
get方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
//设置初始值
return setInitialValue();
}
五、ThreadLocal如何防止内存泄漏
ThreadLocalMap 的每个 Entry 都是一个对键(也就是ThreadLocal变量)的弱引用,这一点从super(k)可看出。另外,每个 Entry 都包含了一个对值的强引用。
static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
//对键的弱引用
super(k);
//对值的强引用
value = v;
}
}
因为对键使用的是弱引用,所以在没有其他强引用指向ThreadLocal变量时,它可被回收,从而避免上文所述ThreadLocal不能被回收而造成的内存泄漏的问题。
但是,这里又可能出现另外一种内存泄漏的问题。ThreadLocalMap 维护 ThreadLocal 变量与具体实例的映射,当 ThreadLocal 变量被回收后,该映射的键变为 null,该 Entry 无法被移除。从而使得实例被该 Entry 引用而无法被回收造成内存泄漏。
针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。