说起本地线程变量,大家首先会想到的是JDK默认提供的ThreadLocal,用来存储在整个调用链中都需要访问的数据,并且是线程安全的,本地变量为线程上下文环境传递提供便捷。
首先大概了解下ThreadLocal 是如何存储值的吧!
Threadlocal 是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据
大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
做个不恰当的比喻,从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象。
这里的这个比喻是不恰当的,实际上是ThreadLocal的静态内部类ThreadLocalMap threadLocals为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。
public T get() {
Thread t = Thread.currentThread(); // @1
ThreadLocalMap map = getMap(t); // @2
if (map != null) { // @3
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // @4
}
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;
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
代码@1:获取当前线程。
代码@2:获取线程的threadLocals属性,在上图中已展示其存储结构。
代码@3:如果线程对象的threadLocals属性不为空,则从该Map结构中,用threadLocal对象为键去查找值,如果能找到,则返回其value值,否则执行代码@4。
代码@4:如果线程对象的threadLocals属性为空,或未从threadLocals中找到对应的键值对,则调用该方法执行初始化。
set方法、remove其实现的逻辑基本一样,就是对线程对象的threadLocals属性进行操作(Map结构)。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
相信大家能很容易看出这些代码,现在我们来做个例子:
public class ThreadLocalService {
private static ThreadLocal<Integer> requestIdThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Integer reqId = new Integer(5);
ThreadLocalService a = new ThreadLocalService();
a.setRequestId(reqId);
}
public void setRequestId(Integer requestId) {
requestIdThreadLocal.set(requestId);
doBussiness();
}
public void doBussiness() {
System.out.println("首先打印requestId:" + requestIdThreadLocal.get());
new Thread(() -> {
System.out.println("子线程启动");
System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());
}).start();
}
}
运行结果你会发现,ThreadLocal 居然不能在主线程和子线程中传递变量。
为了解决该问题,JDK引入了另外一个线程本地变量实现类 InheritableThreadLocal。
InheritableThreadLocal只是在ThreadLocal的get、set、remove流程中,重写了getMap、createMap方法,整体流程与ThreadLocal保持一致,所以接下来我们来看一下InheritableThreadLocal是如何重写上述这两个方法的。
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
从代码得知,
ThreadLocal操作的是Thread对象的threadLocals属性,
InheritableThreadLocal操作的是Thread对象的inheritableThreadLocals属性。
紧接着就来探究InheritableThreadLocal是如何继承自父对象的线程本地变量的,请先看下此段代码
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++;
}
}
}
}
这段代码类似于Map的复制,只不过其在Hash冲突时,不是使用链表结构,而是直接在数组中找下一个为null的槽位。
看到这里我们再来举个InheritableThreadLocal的例子:
public class InheritableThreadLocalService {
/**
* 模拟tomcat线程池
*/
private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);
/**
* 业务线程池,默认Control中异步任务执行线程池
*/
private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);
/**
* 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。
*/
private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
// 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,
// 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量
for (int i = 1; i <= 10; i++) {
tomcatExecutors.submit(new ControlThread(i));
}
//简单粗暴的关闭线程池
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
businessExecutors.shutdown();
tomcatExecutors.shutdown();
}
/**
* 模拟Control任务
*/
static class ControlThread implements Runnable {
private int i;
public ControlThread(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + i);
requestIdThreadLocal.set(i);
//使用线程池异步处理任务
businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));
}
}
/**
* 业务任务,主要是模拟在Control控制层,提交任务到线程池执行
*/
static class BusinessTask implements Runnable {
private String parentThreadName;
public BusinessTask(String parentThreadName) {
this.parentThreadName = parentThreadName;
}
@Override
public void run() {
// System.out.println(Thread.currentThread().getName() + ":****" + requestIdThreadLocal.get());
//如果与上面的能一一对应上来,则说明正确,否则失败
System.err.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());
}
}
}
请注意这个线程:pool-2-thread-3:****8
请注意这个线程:pool-2-thread-5:****5
请注意这个线程:pool-2-thread-4:****4
请注意这个线程:pool-2-thread-2:****7
请注意这个线程:pool-2-thread-1:****6
请注意这个线程:pool-2-thread-2:****7
请注意这个线程:pool-2-thread-4:****4
请注意这个线程:pool-2-thread-5:****5
请注意这个线程:pool-2-thread-3:****8
请注意这个线程:pool-2-thread-1:****6
从执行结果可以看出,当 InheritableThreadLocal 在单线程时没有问题,但是在多线程状态下,因为线程池的复用机制,能减少线程的频繁创建与销毁,线程池中的线程拷贝的数据将来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,因此本地变量值也会是第一个外部线程的值,造成线程本地变量不一致
那我们要如何解决在多线程下数据一一对应呢,此时 阿里巴巴开源的专门解决InheritableThreadLocal的局限性的 TransmittableThreadLocal 闪亮登场
private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder = new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
return new WeakHashMap();
}
protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
return new WeakHashMap(parentValue);
}
};
public final void set(T value) {
super.set(value); // @1
if (null == value) { // @2
this.removeValue();
} else {
this.addValue(); // @3
}
}
private void removeValue() {
((Map)holder.get()).remove(this);
}
private void addValue() {
if (!((Map)holder.get()).containsKey(this)) {
((Map)holder.get()).put(this, (Object)null);
}
}
代码@1:首先调用父类的set方法,将value存入线程本地遍历,即Thread对象的inheritableThreadLocals中。
代码@2:如果value为空,则调用removeValue()否则调用addValue。
代码@3:当前线程在调用threadLocal方法的set方法(即向线程本地遍历存储数据时),如果需要设置的值不为null,则调用addValue方法,将当前ThreadLocal存储到TransmittableThreadLocal的全局静态变量holder。
使用了线程本地变量,内部存放的结构为Map, ?>,即该对象缓存了线程执行过程中所有的TransmittableThreadLocal对象,并且其关联的值不为空。但这样做有什么用呢?
这里就要去了解
@Nonnull
public static Object replay(@Nonnull Object captured) {
Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map)captured;
Map<TransmittableThreadLocal<?>, Object> backup = new HashMap();
Iterator iterator = ((Map)TransmittableThreadLocal.holder.get()).entrySet().iterator();
while(iterator.hasNext()) {
Entry<TransmittableThreadLocal<?>, ?> next = (Entry)iterator.next();
TransmittableThreadLocal<?> threadLocal = (TransmittableThreadLocal)next.getKey();
backup.put(threadLocal, threadLocal.get());
if (!capturedMap.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
setTtlValuesTo(capturedMap);
TransmittableThreadLocal.doExecuteCallback(true);
return backup;
}
@Nonnull
public static Object clear() {
return replay(Collections.emptyMap());
}
public static void restore(@Nonnull Object backup) {
Map<TransmittableThreadLocal<?>, Object> backupMap = (Map)backup;
TransmittableThreadLocal.doExecuteCallback(false);
Iterator iterator = ((Map)TransmittableThreadLocal.holder.get()).entrySet().iterator();
while(iterator.hasNext()) {
Entry<TransmittableThreadLocal<?>, ?> next = (Entry)iterator.next();
TransmittableThreadLocal<?> threadLocal = (TransmittableThreadLocal)next.getKey();
if (!backupMap.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
setTtlValuesTo(backupMap);
}
replay():"重放"父线程的本地环境变量,即使用从父线程中捕获过来的上下文环境,在子线程中重新执行一遍,并返回原先存在与子线程中的上下文环境变量。
restore():恢复线程池中当前执行任务的线程的上下文环境,即代码@1,会直接继承父线程中的上下文环境,但会将原先存在该线程的线程上下文环境进行备份,在任务执行完后通过执行restore方法进行恢复。
capturedMap 子线程从父线程捕获的线程本地遍历。
backup 线程池中处理本次任务的线程中原先存在的本地线程变量。
这个例子只需要将上面的代码替换成 TransmittableThreadLocal 即可,它的实现原理是:
从InheritableThreadLocal不支持线程池的根本原因是InheritableThreadLocal是在父线程创建子线程时复制的,由于线程池的复用机制,减少开销,子线程只会复制一次,要支持线程池中能访问提交任务线程的本地变量,其实只需要在父线程向线程池提交任务时复制父线程的上下环境,那在子线程中就能够如愿访问到父线程中的本地变量,实现本地环境变量在线程池调用中的透传,这也就是TransmittableThreadLocal最本质的实现原理。