多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对同一个共享变量的写入,为了保证线程安全,一般使用者在访问共享变量时需要进行额外的同步措施。ThreadLocal 是除了加锁这种同步方式之外的另一种,规避多线程访问共享变量时发生线程不安全问题的方式。
ThreadLocal 提供线程本地变量,如果创建一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
实现原理
在 Thread 类中有两个变量 threadLocals 和 inheritableThreadLocals,二者都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 就类似于一个 HashMap,可以对其进行 set、get 调用。
默认情况下,每个线程中的这两个变量都为 null,只有当线程第一次调用 ThreadLocal 的 set 或 get 方法时才会创建他们。因此,每个线程的本地变量实际上是放在调用线程的 ThreadLocals 变量里面的。
也就是说,ThreadLocal 类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过 set 方法将 value 添加到调用线程的 threadLocals 中,当调用线程调用 get 方法时能够从它的 threadLocals 中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的 threadLocals 中,所以不使用本地变量时需要调用 remove 方法将 threadLocals 中不用的本地变量删除。
1. set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 获取线程自己的变量threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
每个线程都维护了一个 ThreadLocal.ThradLocalMap 变量,这是一个以 ThreadLocal 对象为键、object 对象为值的存储结构。
2. get
public T get() {
Thread t = Thread.currentThread();
// getMap的源码就是提取线程对象t的ThreadLocalMap属性:t.threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
return (T)e.value;
}
}
return setInitialValue();
}
每个线程都有自己的 ThreadLocalMap,如果 map==null,则直接执行 setlnitialValue()。如果 map 已经创建,就表示 Thread 类的 threadLocals 属性已经初始化,如果 e==null,依然会执行到 setlnitialValue()。
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;
}
// 这个时候由于是初始化,所以默认添加的值为null
protected T initialValue() {
return null;
}
3. remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 移除当前线程中指定ThreadLocal实例的本地变量
m.remove(this);
}
在正常情况下,当线程退出时 Thread 会进行一些清理工作,其中就包括清理 threadLocals。但如果使用线程池的话,当前线程未必会退出。此时,如果将一些比较大的对象设置到 ThreadLocal 中,可能会使系统出现内存泄漏(set后不进行清理)。因此最好使用 remove() 方法将这个变量移除或手动将其设置为 null。
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
target = null;
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
ThreadLocalMap
ThreadLocal 有个静态内部类叫 ThreadLocalMap,ThreadLocalMap 内部由一个 Entry 数组构成,Entry 继承自弱引用 WeakReference,没有方法,只有一个 value 成员变量,它的 key 是 ThreadLocal 对象。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从栈与堆的内存角度看看两者的关系,如下图所示。
图中的红色虚线箭头是重点,也是整个 ThreadLocal 最难以理解的地方。
为什么需要弱引用:
所有 Entry 对象都被 ThreadLocalMap 类的实例化对象 threadLocals 持有,当线程对象执行完毕时,线程对象内的实例属性均会被垃圾回收。但如果线程没有退出,JDK 也允许我们像释放普通变量一样释放 ThreadLocal。比如,有时我们为了加速垃圾回收,会特意写出类似 obj == null 的代码。同理,对于 ThreadLocal 变量,我们也可以手动将其设置为 null。
因为 Entry 是弱引用,即使线程正在执行中,只要我们把 ThreadLocal 对象的引用置为 null,Entry 的 Key 就会自动在下一次 YGC 时被垃圾回收(因为 key 指向 ThreadLocal 对象引用)。而当 ThreadLocal 使用 set() 和 get() 时,又会自动地将那些 key==null 的 value 置为 null,使 value 能够被垃圾回收,避免内存泄漏。
所以设置为弱引用,是为了当 ThreadLocal 对象被置位 null 时可以进行自动回收,但是理想很丰满,现实很骨感,因为 ThreadLocal 对象通常作为私有静态变量使用,则其生命周期不会随线程结束而结束。
使用不当导致内存泄漏的原因:
因为 threadLocals 中的 Entry 的 key 使用的是 ThreadLocal 对象的弱引用,在没有其他地方对 threadLocals 有依赖时,threadLocals 引用的 ThreadLocal 对象就会被回收掉,那么 Entry 中的与之关联的弱引用 key 就会变成 null,如果此时当前线程还在运行,则 Entry 对应的 value 不会被回收,因为存在强引用,这就发生了内存泄漏。当然这种内存泄漏分情况,如果当前线程执行完毕会被回收,那么 Value 自然也会被回收,但如果使用线程池则 Value 会一直存在,这就发生了内存泄漏。因此,我们在使用完毕时要及时调用 remove 方法以避免内存泄漏。
InheritableThreadLocal
同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。因为 threadLocals 中为当前调用线程对应的本地变量,所以二者自然是不能共享的。而 InheritableThreadLocal 则可以做到这个功能。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
从上面代码可以看出,InheritableThreadLocal 类继承了 ThreadLocal 类,并重写了 childValue、getMap、createMap 三个方法。其中 createMap 方法在被调用时,创建的是 inheritableThreadLocal。同理,getMap 方法在当前调用者线程调用 get 方法时返回的也是 inheritableThreadLocal。
下面看看重写的 childValue 方法在什么时候执行,怎样让子线程访问父线程的本地变量值。首先从 Thread 类的 init 方法开始说起,该方法会在新建 Thread 实例时被执行:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
// 获取当前线程,即新建该线程的主线程
Thread parent = currentThread();
......
// 如果父线程的inheritableThreadLocal不为null
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID();
}
// 相当于复制一份
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
可以看到,子类获取到的父类变量实际上是复制了一份。