1、概述

ThreadLocal(TL)是Java中一种线程局部变量实现机制,他为每个线程提供一个单独的变量副本,保证多线程场景下,变量的线程安全。经常用于代替参数的显式传递。

InheritableThreadLocal(ITL)是JDK提供的TL增强版,而TransmittableThreadLocal(TTL)是阿里开源的ITL增强版

这些ThreadLocal在不同场景下有不同用途,我们来分析一下:

2、ThreadLocal

ThreadLocal主要的方法有四个:initialValue、set、get、remove

2.1、初始化——initialValule

当线程首次访问该ThreadLocal时(ThreadLocal.get()),会进行初始化赋值。我们常用两种方法初始化ThreadLocal

2.1.1、重写initialValue

ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "";
}
};

2.1.2、调用ThreadLocal.withInitial

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "");

他会创建一个SuppliedThreadLocal内部类

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}

该类重写了initialValue方法

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
} @Override
protected T initialValue() {
//当该线程首次访问ThreadLocal时,会间接调用lambda表达式初始化
return supplier.get();
}
}

️ITL并没有重新实现withInitial,如果使用withInitial则会创建STL,失去自己增强的特性

2.2、赋值——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);
}

这里出现了一个关键属性ThreadLocalMap,类定义在ThreadLocal中,是Thread的成员变量

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

ThreadLocalMap内部还有一个内部类Entry,是存值的地方

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
//ThreadLocal的引用是“key”
super(k);
//线程局部变量是value
value = v;
}
}
//Entry数组
//value具体放在哪个index下,是由ThreadLocal的hashCode算出来的
private Entry[] table;
}

2.3、取值——get

public T get() {
Thread t = Thread.currentThread();
//1、获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//2、根据ThreadLocal的hashCode,获取对应Entry下的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//3、如果没有赋过值,则初始化
return setInitialValue();
}

2.4、清空——remove

 public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//会将对应Entry、包括他的key、value手动置null
m.remove(this);
}

3、InheritableThreadLocal

3.1、TL在父子线程场景下存在的问题

我们先来看一个例子

public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "A");
threadLocal.set("B");
Thread thread = new Thread(() -> {
System.out.println("子线程ThreadLocal:" + threadLocal.get());
}, "子线程");
thread.start();
thread.join();
}

打印结果如下,可见子线程的ThreadLocal是初始值,并没有使用父线程修改后的值:

子线程ThreadLocal:A

线程的ThreadLocalMap是首次访问时创建的,所以子线程使用ThreadLocal的时候,会初始化一个新的ThreadLocal,线程局部变量为默认值

️所以,TL不具有遗传性

3.2、ITL的解决方案

为了解决TL子线程遗传性的问题,JDK引入了ITL

他继承ThreadLocal,重写了childValue、getMap、createMap三个方法

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);
}
}

这里出现了inheritableThreadLocals,他存储的就是从父线程拷贝过来的ThreadLocal,这个值是在父线程首次修改ThreadLocal的时候赋值的,然后在子线程创建时拷贝过来的

//父线程部分:
public void set(T value) {
Thread t = Thread.currentThread();
//该方法被ITL重写,访问inheritableThreadLocals为null
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//该方法同样被ITL重写,创建一个ThreadLocalMap赋值给inheritableThreadLocals
createMap(t, value);
} //子线程部分:
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();
//1、允许ThreadLocal遗传(这个默认为true)
//2、inheritableThreadLocals不为空,因为父线程调用set了
//父线程不调用set,那ThreadLocal就是初始值,那直接初始化就好了,也不用进该分支
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
} //createInheritedMap使用该构造函数,根据父线程的inheritableThreadLocals进行深拷贝
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
//深拷贝父线程ThreadLocalMap
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) {
//childValue被ITL重写,返回父线程ThreadLocal的值
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++;
}
}
}
}

使用ITL的效果

public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() {
@Override
protected String initialValue() {
return "A";
}
};
threadLocal.set("B");
Thread thread = new Thread(() -> {
System.out.println("子线程ThreadLocal:" + threadLocal.get());
}, "子线程");
thread.start(); thread.join();
}

打印结果如下,子线程拷贝了父线程ThreadLocal:

子线程ThreadLocal:B

总结一下,ITL解决父子线程遗传性的核心思路是,将可遗传的ThreadLocal放在父线程新的ThreadLocalMap中,在子线程首次使用时进行拷贝

4.、TransmittableThreadLocal

4.1、ITL在线程复用场景下存在的问题

我们再从一个简单的例子说起

public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() {
@Override
protected String initialValue() {
return "A";
}
};
threadLocal.set("B");
ExecutorService executorService = Executors.newFixedThreadPool(1);
//1、子线程第一次获取ThreadLocal
executorService.submit(() -> System.out.println("子线程ThreadLocal:"+threadLocal.get())).get();
Thread.sleep(1000);
//2、父线程修改ThreadLocal
threadLocal.set("C");
System.out.println("父线程修改ThreadLocal为"+threadLocal.get());
//3、子线程第二次获取ThreadLocal
executorService.submit(() -> System.out.println("子线程ThreadLocal:"+threadLocal.get())).get();
}

打印结果如下,子线程在第二次打印时,并没有拷贝父线程的ThreadLocal,使用的还是首次拷贝的值:

子线程ThreadLocal:B
父线程修改ThreadLocal为C
子线程ThreadLocal:B

️可复用的子线程不会感知父线程ThreadLocal的变化

4.2、TTL的解决方案

4.2.1、TTL的使用

TTL在ITL上做了稍微复杂的封装,我们从使用开始了解

引入依赖

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>latest</version>
</dependency>

在使用TTL时,线程需要经过TTL封装,线程池同理

public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadLocal<String> threadLocal = new TransmittableThreadLocal<String>() {
@Override
protected String initialValue() {
return "A";
}
};
threadLocal.set("B");
ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get())).get();
Thread.sleep(1000);
threadLocal.set("C");
System.out.println("父线程修改ThreadLocal为" + threadLocal.get());
executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get())).get();
Thread.sleep(1000);
executorService.submit(() -> {
threadLocal.set("D");
System.out.println("子线程修改ThreadLocal为" + threadLocal.get());
});
Thread.sleep(1000);
executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get()));
Thread.sleep(1000);
}

打印结果如下,子线程每次都会获取父线程的ThreadLocal

子线程ThreadLocal:B
父线程修改ThreadLocal为C
子线程ThreadLocal:C
子线程修改ThreadLocal为D
子线程ThreadLocal:C

从使用上看,TTL要求将任务封装,那我们就从ThreadLocal和ExecutorService两部分入手

4.2.2、TTL对ThreadLocal的封装

下面是TTL的取值和赋值逻辑,都涉及一个关键方法addThisToHolder,对应的属性holder会在线程池执行任务时用到

//TransmittableThreadLocal.addThisToHolder()
private void addThisToHolder() {
//InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder
if (!holder.get().containsKey(this)) {
//holder是静态变量,他会把TTL存到当前线程的map中
//value是null,他其实是把Map当Set用
//主线程赋值时,会获取主线程的holderMap,然后把TTL存进去
holder.get().put((TransmittableThreadLocal<Object>) this, null);
}
} @Override
public final void set(T value) {
if (!disableIgnoreNullValueSemantics && null == value) {
remove();
} else {
super.set(value);
//当主线程赋值时,会将自己的TTL放到自己的map中
addThisToHolder();
}
} @Override
public final T get() {
T value = super.get();
if (disableIgnoreNullValueSemantics || null != value)
addThisToHolder();
return value;
}

4.2.3、TTL对任务的封装

//我们通过TtlExecutors.getTtlExecutorService()对线程池进行封装
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {
if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {
return executorService;
}
//入参是线程池,通过包装类代理线程池的操作
return new ExecutorServiceTtlWrapper(executorService);
} //ExecutorServiceTtlWrapper.submit()
public Future<?> submit(@NonNull Runnable task) {
//将提交的任务进行封装
return executorService.submit(TtlRunnable.get(task));
}

4.2.3.1、任务构建

TtlRunnable构造方法

这里都是主线程在操作,因为任务是主线程提交的

private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
this.capturedRef = new AtomicReference<Object>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}

这里有一个关键属性capturedRef,他是一个原子引用,存了TTL

//TrasmitableThreadLocal.Transmitter
public static Object capture() {
//获取ttl的值构建快照
return new Snapshot(captureTtlValues(), captureThreadLocalValues());
} private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>();
for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
//将主线程TTL的值存到当前任务中
ttl2Value.put(threadLocal, threadLocal.copyValue());
}
return ttl2Value;
}

4.2.3.2、任务执行

任务执行的代码如下,在任务执行前回放ThreadLocal,在任务执行后恢复ThreadLocal:

这里都是子线程在操作,因为任务都是子线程执行的

@Override
public void run() {
Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
//1、备份子线程ThreadLocal
//2、使用主线程提交任务时构建的ThreadLocal副本,将子线程ThreadLocal覆盖
Object backup = replay(captured);
try {
//3、任务执行
runnable.run();
} finally {
//3、使用之前备份的子线程ThreadLocal进行恢复
restore(backup);
}
}

总结一下,TTL让子线程感知父线程变化的核心思路是,主线程在任务提交时构建ThreadLocal副本,在子线程执行任务时供其使用

️提交和执行任务会对TTL进行若干操作,理论上对性能有一点点影响,官方性能测试结论说损耗可忽略

TTL官方性能测试

作者:京东物流 刘朝永

来源:京东云开发者 自猿其说

从TL、ITL到TT的更多相关文章

  1. CF533F Encoding 题解

    题目链接CF533F Encoding 提示1:   \(\mathcal O(26^2*n)\) 的算法可通过.常用的几种字符串匹配算法kmp,AC自动机,哈希都可以解决该问题 (后两者可以优化到 ...

  2. 【网络流24题】No.21 (最长 k 可重区间集问题 最长不相交路径 最大费用流)

    [] 输入文件示例input.txt4 21 76 87 109 13 输出文件示例output.txt15 [分析] 直接co题解好了,写得挺全.. [建模方法] 方法1 按左端点排序所有区间,把每 ...

  3. Sybase数据库实现等效的mysql中group_concat功能

    在MySQL中,如果想实现将分组之后的多个数据合并到一列,可以使用group_concat函数,如下图所示: 但是,在Sybase中没有这样的函数(别问我为什么使用Sybase,因为公司用的Sybas ...

  4. [ST表/贪心] NOI2010 超级钢琴

    [NOI2010]超级钢琴 题目描述 小Z是一个小有名气的钢琴家,最近C博士送给了小Z一架超级钢琴,小Z希望能够用这架钢琴创作出世界上最美妙的音乐. 这架超级钢琴可以弹奏出n个音符,编号为1至n.第i ...

  5. hdu 2141 Can you find it? (二分法)

    Can you find it? Time Limit: 10000/3000 MS (Java/Others)    Memory Limit: 32768/10000 K (Java/Others ...

  6. bzoj 2093 [Poi2010]Frog——滑动窗口

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=2093 找第k近的可以用一个含k个元素的滑动窗口来实现. 卡空间也还行,但卡时间.不要预处理倍 ...

  7. SQL Server 存储过程遇到“表 '#TT' 没有标识属性。无法执行 SET 操作”错误

    创建临时表,往临时表插入数据的时候报的错误. 一开始提示没有打开主键,后来打开主键就提示上述错误异常. 从网上查找资料没有找到,然后又到群里问各位大牛,一位大牛告诉我是没有设置主键. 我又仔细看看提示 ...

  8. webapi修改tt模板给字段添加JsonIgnore特性解决转换json循环引用问题

    0.问题描述 EF生成的model带有导航属性,则json序列化会报循环引用错误,尝试如下 protected void Application_Start() { GlobalConfigurati ...

  9. Oracle ITL(Interested Transaction List)理解

    ITL(Interested Transaction List) ITL是位于数据块头部的事物槽列表,它是由一系列的ITS(Interested Transaction Slot,事物槽)组成,其初始 ...

  10. 报错:org.hibernate.AssertionFailure: null id in com.tt.hibernate.entities.News entry (don't flush the Session after an exception occurs)

    在使用hibernate创建数据库的表格时,出现了如下报错: 十二月 28, 2016 10:17:02 上午 org.hibernate.tool.hbm2ddl.SchemaExport perf ...

随机推荐

  1. FBV和CBV的区别(源码分析)

    FBV和CBV源码分析 FBV直接调用user方法执行业务代码 CBV相当于在FBV上面封装了一层 from django.contrib import admin from django.urls ...

  2. 省市县树形结构打印-.netCore控制台程序

    using CityJson;using Dapper;using Newtonsoft.Json;{ using (var db = DbHelper.Db()) { //数据格式 //code_p ...

  3. This application failed to start because it could not find or load the Qt platforms plugins

     由于一直在linux下操作,今天Qt移植平台的时候导致.exe可执行文件一直运行不起来,提示缺少某些dll库,这个问题解决起来简单(直接去qt源码里面查找对应库添加到可执行文件目录就行),但是之后一 ...

  4. vue移动端预览 pdf 文件

    pdf预览,在项目中是很常见的需求,在PC端web项目中,我们可以使用window.open(url)直接打开pdf进行预览,那么移动端虽然我们也可以使用此方法,但是这是新开了一个webview页面, ...

  5. 「学习笔记」tarjan求最近公共祖先

    Tarjan 算法是一种 离线算法,需要使用并查集记录某个结点的祖先结点. 并没有传说中的那么快. 过程 将询问都记录下来,将它们建成正向边和反向边. 在 dfs 的过程中,给走过的节点打上标记,同时 ...

  6. Redis之消息队列实现

    文章目录 秒杀场景 采用消息队列实现 List实现消息队列 PubSub(发布订阅)实现消息队列 基于Stream实现消息队列 消费者组 实践 总结 秒杀问题是非常重要且比较难实现的,如果不进行架构的 ...

  7. Redis分布式锁实现及使用

    文章目录 分布式锁 全局ID生成器 一人一单实现 超卖问题 一人一单 分布式锁 Redis setnx实现分布式锁 Redis在业内解决秒杀等业务场景有非常广的应用,如何设计实现一个分布式锁是解决超卖 ...

  8. boot-admin开源项目中有关后端参数校验的最佳实践

    我们在项目开发中,经常会对一些参数进行校验,比如非空校验.长度校验,以及定制的业务校验规则等,如果使用if/else语句来对请求的每一个参数一一校验,就会出现大量与业务逻辑无关的代码,繁重不堪且繁琐的 ...

  9. spring xml配置中引用java配置不能用ClassPathXmlApplicationContext

    现在的目的是想测试在xml配置中引用java配置的bean CD唱片的接口: package v4.c2; public interface CompactDisc { void play(); } ...

  10. Unity快速接入bugly, 支持Unity2021

    鹅厂提供的bugly官方demo工程打包后台也查不到日志,N年不更新(官方已经说不再维护),为此本人做了部分修改测试,提供一个快速接入工程的demo. Unity2021因为版本原因腾讯官方工程不能使 ...