一、ThreadLocal概述
定义:threadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类; 当使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。
结构:
threadlocal是解决线程安全的一种有效的方式,但是从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争
二、ThreadLocal 实现原理 ⭐
通过实例代码来简单演示下ThreadLocal的使用。
复制代码
/**
* @BelongsProject: threadlocal
* @Author: xc
* @CreateTime: 2023-10-27 13:39
* @Description: TODO
* @Version: 1.0
*/
public class TestThreadLocal {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
try {
threadLocal.set("aaa");
threadLocal1.set("aaa1");
threadLocal2.set("aaa2");
System.out.println("threadA-threadLocal:" + threadLocal.get());
System.out.println("threadA-threadLocal1:" + threadLocal1.get());
System.out.println("threadA-threadLocal2:" + threadLocal2.get());
threadLocal.remove();
System.out.println("threadA-threadLocal:" + threadLocal.get());
System.out.println("threadA-threadLocal1:" + threadLocal1.get());
System.out.println("threadA-threadLocal2:" + threadLocal2.get());
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
threadLocal.set("bbb");
System.out.println("threadB:" + threadLocal.get());
threadLocal.remove();
System.out.println("threadB:" + threadLocal.get());
}).start();
}
}
threadA-threadLocal:aaa
threadA-threadLocal1:aaa1
threadA-threadLocal2:aaa2
threadA-threadLocal:null
threadA-threadLocal1:aaa1
threadA-threadLocal2:aaa2
threadB:bbb
threadB:null
在日常开发中简单的使用一般使用threadlocal的get set remove,最常用的方法这里就不分析了,主要分析threadlocal的设计理念和对应的一些不常用的方法解析以及threadlocal使用的安全隐患
InheritableThreadLocal
在实际开发过程中,我们可能会遇到这么一种场景。主线程开了一个子线程,但是我们希望在子线程中可以访问主线程中的ThreadLocal对象,也就是说有些数据需要进行父子线程间的传递。比如像这样:
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
IntStream.range(0,10).forEach(i -> {
//每个线程的序列号,希望在子线程中能够拿到
threadLocal.set(i);
//这里来了一个子线程,我们希望可以访问上面的threadLocal
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
执行上述代码,你会看到:
Thread-0:null
Thread-1:null
Thread-2:null
Thread-3:null
因为在子线程中,是没有threadLocal的。如果我们希望子线可以看到父线程的ThreadLocal,那么就可以使用InheritableThreadLocal。顾名思义,这就是一个支持线程间父子继承的ThreadLocal,将上述代码中的threadLocal使用InheritableThreadLocal:
InheritableThreadLocal threadLocal = new InheritableThreadLocal();
再执行,就能看到:
Thread-0:0
Thread-1:1
Thread-2:2
Thread-3:3
Thread-4:4
可以看到,每个线程都可以访问到从父进程传递过来的一个数据。
原理
package java.lang;
import java.lang.ref.*;
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
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。
如何让子线程可以访问父线程的本地变量。
这要从创建Thread的代码说起,打开Thread类的默认构造函数,代码如下。
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ... 省略无关部分
Thread parent = currentThread();
// ... 省略无关部分
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// ... 省略无关部分
}
再来看看里面是如何执行createInheritedMap 的。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (Entry e : parentTable) {
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 对象中,threadLocal 实现线程内部变量共享,InheritableThreadLocal 实现了父线程与子线程的变量继承。
虽然InheritableThreadLocal看起来挺方便的,但是依然要注意以下几点:
变量的传递是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不灵了,也就是在使用线程池等会池化复用线程的执行组件情况下,异步执行执行任务,需要传递上下文的情况。
针对上述情况,阿里开源了一个
TTL
库,即Transmittable ThreadLocal来解决这个问题,有兴趣的朋友们可以去看看。变量的赋值就是从主线程的map复制到子线程,它们的value是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题
threadLocals
原理
我们先看下ThreadLocal 与 Thread 的类图,了解他们的主要方法和相互之间的关系。
图中几个类我们标注一下:
Thread
ThreadLocal
SuppliedThreadLocal
ThreadLocalMap
ThreadLocalMap.Entry
接下去,我们首先先开始了解这几个类的相互关系:
Thread 类中有一个 threadLocals 成员变量(实际上还有一个inheritableThreadLocals上面所示),它的类型是ThreadLocal 的内部静态类ThreadLocalMap
ThreadLocalMap 是一个定制化的Hashmap,为什么是个HashMap?很好理解,每个线程可以关联多个ThreadLocal变量。
ThreadLocalMap 初始化时会创建一个大小为16的Entry 数组,Entry 对象也是用来保存 key- value 键值对(这个Key固定是ThreadLocal 类型)。值得注意的是,这个Entry 继承了
WeakReference
(这个设计是为了防止内存泄漏,后面会讲)从源码中可以看出来,自始至终,这些本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量,那个线程私有的threadLocalMap 里面。
ThreadLocal就是一个工具壳和一个key,它通过set方法把value值放入调用线程的threadLocals里面并存放起 来, 当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。
withInitial解析
由于ThreadLocal里设置的值,只有当前线程自己看得见,这意味着你不可能通过其他线程为它初始化值。为了弥补这一点,ThreadLocal提供了一个withInitial()方法统一初始化所有线程的ThreadLocal的值,案例如下:
public class InnerClass {
public static void main(String[] args) {
ThreadLocal<Integer> localInt = ThreadLocal.withInitial(new Supplier<Integer>() {
@Override
public Integer get() {
System.out.println("执行了初始化方法");
return 6;
}
});
new Thread(() -> {
try {
String name = localInt.getClass().getName();
Integer integer = localInt.get();
System.out.println(integer);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
当threadlocal获取初始化值的时候调用链路为
threadlocal中get()->setInitialValue()->setInitialValue方法中initialValue()(注意:此时threadlocal类型为SuppliedThreadLocal为ThreadLocal中的内部类也为它的子类,覆盖重写了ThreadLocal的初始化方法initialValue())
在调用supplier.get()方法时真正实现就是自定义的初始化的withInitial声明定义中的get值,方便各个线程统一初始化
ThreadLocalMap解析
如上图所示,threadlocalmap的引用结构图已经很详细的展示详细可参考:
https://www.zhihu.com/question/477411483?ssr_src=heifetz&utm_id=0
https://blog.csdn.net/cj_eryue/article/details/112857999
知识点
ThreadLocal 的主要问题是会产生脏数据和内存泄露。https://juejin.cn/post/6844904102015713293
评论区