ThreadLocal

摘自《Java并发编程之美》再结合一些自己的理解

简介

多线程访问同一个共享变量时特别容易出现并发问题,特别时在多个线程需要对同一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,如图。

image-20220829160953636

同步的措施一般是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担。那么有没有一种方式可以做到,当创建一个变量后,每个线程对其访问的时候访问的是自己线程的变量呢?其实 ThreadLocal 就可以做这件事,虽然 ThreadLocal 并不是为了解决这个问题而出现的。

ThreadLocalJDK 包提供的,他提供了线程本地变量,也就是如果你创建了一个 ThreadLocal 变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个 ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存(也就是如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有该变量的一个副本),如图。

image-20220829161409978

使用示例

本例开启了两个线程,在每个线程内部都设置了本地变量的值,然后调用 print 函数打印当前本地变量的值。如果打印后调用了本 变量的remove 方法, 删除本地内存中的该变量,代码如下。

public class ThreadLocalDemo {
//print函数
public static void print(String str){
//打印当前线程本地内存中的localVariable变量的值
System.out.println(str +":"+ localVariable.get());
//清除当前线程本地内存中的localVariable变量
localVariable.remove();
}

//创建ThreadLocal变量
public static ThreadLocal<String> localVariable = new ThreadLocal<>();

public static void main(String[] args) {

//创建线程one
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
//设置线程One中本地变量localVariable的值
localVariable.set("threadOne local Variable");
//调用打印函数
print("threadOne");
//打印本地变量值
System.out.println("threadOne remove after" + ":" + localVariable.get());
}
});

//创建线程two
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
//设置Two中本地变量localVariable的值
localVariable.set("threadTwo local variable");
//调用打印函数
print("threadTwo");
//打印本地变量值
System.out.println("threadTwo remove after" + ":" + localVariable.get());
}
});

//启动线程
threadOne.start();
threadTwo.start();

}
}

执行结果

image-20220829163719610

线程One中的代码通过 set 方法设置了 localVariable 的值,这其实设置的是线程One 本地内存中的一个副本,这个副本线程 Two 是访问不了的。然后调用了print函数,通过 get 函数取得了当前线程本地内存中 localVariable 的值。线程Two执行同理

实现原理

ThreadLocal 相关类的类图结构

image-20220829172812762

由图可知,Thread类中有一个 threadLocals 变量和与一个 inheritableThreadLocals变量,他们都是 ThreadLocalMap 类型的变量 。在默认情况下,每个线程中的的这两个变量都为null,只有当线程第一次调用 ThreadLocal 的 set 或者get 方法时才会创建它们。

ThreadLocalMap 是一个定制化的Hashmap

其实当我们调用 ThreadLocal 的 set 方法存储本地变量时,并不存放在 ThreadLocal 实例里面,而是存放在调用线程的 threadLocals 变量里面。也就是说,**ThreadLocal 类型实例的本地变量存放在具体的线程空间中**。

Thread 就是一个工具壳,他并不是正真存放变量的地方,而是通过 set 方法和 get 方法 把 value 值存放到调用线程的 threadLocals 里面存放和从中拿出来。

如果调用的线程一直不终止,那么这个本地变量会一直存放在调用线程的 threadLocals 变量里面,所以当不需要使用本地变量时可以通过调用 ThreadLocal 变量的remove方法,从当前线程的 threadLocals 中删除该本地变量。

Thread 里面的 threadLocals 之所以被设计为 map 结构,是因为每个线程可以关联多个 ThreadLocal 变量

ThreadLocal 数据结构

API分析

void set(T value)

public void set(T value) {
//获取当前调用线程
Thread t = Thread.currentThread();
//通过当前线程,去查找对应的线程变量,找到则设置
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
//第一次调用就创建当前线程对应的HashMap
createMap(t, value);
}
}

可以发现上述代码中一个段是根据当前调用线程,获取map,调用了 getMap(t) 方法

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

该方法的作用时获取传入线程的 threadLocals 变量,threadlocal 变量被绑定到了线程的成员变量上。

再回到 set 方法,如果 getMap(t) 返回值不为空,就把 value 值设置到 threadLocals 中。 threadLocals 是一个 HashMap 结构,其中 key 就是当前 ThreadLocal 的实例对象引用,value 是通过 set 方法传递的值

如果 getMap(t) 返回值为空,则说明是第一次调用 set 方法,这时会调用 createMap(t, value) 初始化当前线程的 threadLocals 变量。

void createMap(Thread t, T firstValue) {
//创建当前线程的 ThreadLocals 变量,并将 value 填入
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

T get()

public T get() {
//获取当前调用的线程
Thread t = Thread.currentThread();
//获取当前线程的 threadLocals 变量
ThreadLocalMap map = getMap(t);
//如果threadLocals 不为null, 则返回对应本地变量(ThreadLocal的实例)的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
//如果获取到的变量值为空 也会进行初始化值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//threadLocals为空则初始化当前线程的threadLocals成员变量
return setInitialValue();
}

同样也是获得当前调用的线程,通过该线程去获得线程内部的 threadLocals 变量,如果 getMap(t) 返回值不为空,再根据当前 ThreadLocal 实例作为key去获取 threadLocals 中对应的值。

如果 getMap(t) 返回值为空或者获取到的变量值为空,则执行 setInitialValue() 方法进行初始化。

private T setInitialValue() {
//初始化值为null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果当前线程的threadLocals变量不为空
if (map != null) {
map.set(this, value);
} else {
//如果当前线程的threadLocals变量为空
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}

protected T initialValue() {
return null;
}

如果当前线程的 threadLocals 变量不为空,则设置当前线程本地变量值为null,否则调用 createMap 方法创建当前线程的 threadLocals 变量。


void remove()

public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}

如果当前线程的 threadLocals 变量不为空,则删除当前线程中指定 ThreadLocal 实例对应的本地变量

总结

每个线程内部都有一个名为 threadLocals 的成员变量,该变量的类型是 HashMap ,其中 key 为我们定义的 ThreadLocal 变量的 this 引用, value 则为我们使用 set 方法设置的值。每个线程的本地变量存放在线程自己的内存变量 threadLocals 中。

如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用 ThreadLocal 的 remove 方法删除对应线程的 threadLocals 中的本地变量。

image-20220829183434665

这个图很形象的介绍了内存关系。


TheadLocal 不支持继承性

首先看一个例子

public class ThreadLocalDemo02 {
//创建线程变量
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

public static void main(String[] args) throws InterruptedException {
//设置线程变量
threadLocal.set("hello world");

//启动子线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//子线程输出线程变量的值
System.out.println("thread:" + threadLocal.get());
}
});

thread.start();
//阻塞主线程 使其等待子线程执行完毕
thread.join();
//主线程输出线程变量的值
System.out.println("main:"+threadLocal.get());
}
}

输出结果如下:

image-20220829184612629

原因就是父子线程并不是同一个线程,所以获取不到父线程 threadLocals 中的本地变量。

那么有没有办法让子线程能访问到父线程中的值? 答案是有。**InheritableThreadLocal类**


lnheritableThreadLocal类

为了解决子线程无法访问父线程中值的问题,InheritableThreadLocal 应运而生。InheritableThreadLocal 继承自 ThreadLocal , 其提供一个特性,就是让子线程可以访问在父线程中设置的本地变量。下面是 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,并重写了三个方法。由上述代码可知,InheritableThreadLocal **重写了 createMap 方法,现在当第一次调用 set 方法时,创建的是当前线程的 inheritableThreadLocals 变量的实例而不是 threadLocals**。

当调用 get 方法时获取当前线程内部的 map 变量时,获取的是 inheritableThreadLocals 变量的实例而不再是 threadLocals

综上可知,在 InheritableThreadLocal中,变量 inheritableThreadLocals 代替了 threadLocals


接下来,我要揭秘 InheritableThreadLocal 让子线程可以访问父线程的本地变量。这要从创建 Thread 的代码说起,打开 Thread 类的默认构造函数,代码如下。

public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}


private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {

......

//获取当前调用的线程
Thread parent = currentThread();

......

//如果父线程(当前调用的线程)的 inheritableThreadLocals 变量不为null 并且 inheritThreadLocals 变量为true
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//设置子线程中的 inheritableThreadLocals 变量
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;

/* Set thread ID */
tid = nextThreadID();
}


如上代码在创建线程时,在构造函数里面会调用 init 方法。代码中获取了当前线程(父线程,这里是 main 函数所在的线程),然后判断 main 函数所在线程里面的 inheritableThreadLocals 属性是否为空以及 inheritableThreadLocals 变量是否为 true,如果都满足就会执行 createInheritedMap() 方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}

可以看到,在方法内部使用父线程的 inheritableThreadLocals 变量作为构造函数的变量创建了一个新的 ThreadLocalMap 变量,然后赋值给了子线程的 inheritableThreadLocals 变量。

下面我们看看 ThreadLocalMap 的构造函数内部都做了什么

private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
//调用重写的方法
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}

在该构造函数内部把父线程的 inheritableThreadLocals 成员变量的值复制到了新的 ThreadLocalMap对象中 , 其中调用了 InheritableThreadLocal 类重写的代码

总结

InheritableThreadLocal 类通过重写代码让本地变量保存到了具体线程的 inheritableThreadLocals 变量里面。

线程在通过 InheritableThreadLocal 类实例的 set 或者 get方法设置变量时,就会创建当前线程的 inheritableThreadLocals 变量。当父线程创建子线程时,构造函数会把父线程中 inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的 inheritableThreadLocals 变量里面。

注意:只会在初始化线程的时候才会把父线程中 inheritableThreadLocals 内的本地变量复制到子线程中。线程一旦初始化后,父线程再调用 set 或者 remove 方法修改 inheritableThreadLocals 是不影响子线程的。

修改原来代码

//创建线程变量
public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

输出结果

image-20220829193247122

可见,现在可以从子线程正常获取到父线程变量的值了。

其实子线程使用父线程中的 threadlocal 方法有很多种,比如:

  • 创建线程时,传入父线程中的变量,并将其复制到子线程中
  • 在父线程中构造一个 map 作为参数传递给子线程

但这些方法都改变了我们的使用习惯,所以在这些情况下就显得 InheritableThreadLocal 比较有用。

使用场景

  • 子线程需要使用存放在 threadLocal 变量中的用户登录信息
  • 中间件需要把统一的 id 追踪的整个调用链路记录下来。

ThreadLocal内存泄漏

强引用与弱引用

img

实线代表强引用,虚线代表弱引用

  • 每一个Thread维护一个 ThreadLocalMap, key为使用弱引用的 ThreadLocal 实例,value为线程变量的副本。

  • 强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不回收这种对象。

  • 如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使 JVM 在合适的时间就会回收该对象。

  • 弱引用,JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在 java 中,用java.lang.ref.WeakReference 类来表示。

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

ThreadLocal 内存泄露问题是怎么导致的?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

//弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}