ThreadLocal及其扩展

简介: ThreadLocalThreadLocal是线程本地变量,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

ThreadLocal

ThreadLocal是线程本地变量,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap参照HashMap的实现,ThreadLocalMap的key为ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

ThreadLocalMap

ThreadLocalMap是ThreadLocal的静态内部类。ThreadLocalMap提供了一种为ThreadLocal定制的高效实现,并且自带一种基于弱引用的垃圾清理机制。
ThreadLocalMap是类似HashMap实现的另一种map实现,有自己的key和value,key为ThreadLocal,value为代码中放入的值。和HashMap一样有一个table数据,有get和set方法,在key的hash冲突时往下寻找对应对象槽位。
ThreadLocalMap里的节点Entry不同于HashMap里面的Entry,继承弱引用,是如下定义的。

static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
   // 往ThreadLocal里实际塞入的值
   Object value;
   Entry(java.lang.ThreadLocal<?> k, Object v) {
       super(k);
       value = v;
   }
}

为什么用弱引用
弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
ThreadLocal的get和set方法

  public T get() {
        Thread t = Thread.currentThread(); //获取当前线程
        ThreadLocalMap map = getMap(t); // 获取当前线程对象的实例变量threadLocals
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

set方法逻辑同get方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

InheritableThreadLocal

InheritableThreadLocal继承ThreadLocal,ThreadLocal在线程内实现一个局部变量,可以在线程的任何地方来访问,能够减少参数的传递,InheritableThreadLocal在子线程和父线程之间共享线程实例本地变量,也同样是为了减少参数的传递。
ThreadLocal使用的是Thread的实例变量threadLocals;InheritableThreadLocal使用的是Thread的实例变量inheritableThreadLocals。这两个变量类型都为ThreadLocal.ThreadLocalMap,用途不一样。
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重载了getMap和createMap方法,操作的不再是threadLocals,而是inheritableThreadLocals。
在Thread初始化时有以下逻辑。

if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这个保证了inheritableThreadLocals变量从父Thread传递到子Thread。

Transmittable ThreadLocal(TTL)

ThreadLocal实现了线程本地变量隔离,每个线程可以拥有不会被别的线程干扰的线程值map。
InheritableThreadLocal继承ThreadLocal,InheritableThreadLocal可以在子线程和父线程之间共享线程实例本地变量。
JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的线程的ThreadLocal值传递到 任务执行时的Thread。
这种场景需要TransmittableThreadLocal类继承并加强InheritableThreadLocal类,解决上述的问题。
框架/中间件集成TTL(TransmittableThreadLocal)传递,通过TransmittableThreadLocal.Transmitter 抓取当前线程的所有TTL值并在其他线程进行回放;在回放线程执行完业务操作后,恢复为回放线程原来的TTL值。

TransmittableThreadLocal.Transmitter提供了所有TTL值的抓取、回放和恢复方法(即CRR操作):

  1. capture方法:抓取线程(线程A)的所有TTL值。
  2. replay方法:在另一个线程(线程B)中,回放在capture方法中抓取的TTL值(回放的过程也就是把第一步抓取的线程实例本地变量设置到当前线程的实例本地变量中),并返回 回放前TTL值的备份
  3. restore方法:恢复线程B执行replay方法之前的TTL值(即备份,把第二步返回的回放前TTL值重新设置回池化中线程的实例本地变量)

整个过程的完整时序图

img_b6983cc61069d999671ba861ee0ced0a.png
image.png

原理简析
对Runnable和Callable进行装饰,形成TtlRunnable和TtlCallable,在这两个装饰类里面主要多了一个引用对象(存放线程切换时线程的本地变量引用)。

private final AtomicReference<Object> capturedRef; //用户存放线程本地变量的map引用

再来看一下CRR操作的核心源码,CRR的代码逻辑时在TransmittableThreadLocal的静态内部类Transmitter中:
capture

public static Object capture() {
            Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
            //holder是TransmittableThreadLocal类的静态对象,类型为InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>,用于存储线程的本地变量,这一份本地变量被TransmittableThreadLocal用于CRR操作
            for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
                captured.put(threadLocal, threadLocal.copyValue());
            }
            return captured;
        }

repaly

public static Object replay(Object captured) {
            @SuppressWarnings("unchecked")
            Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured;
            Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>();

            for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
                 iterator.hasNext(); ) {
                Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
                TransmittableThreadLocal<?> threadLocal = next.getKey();
                // backup
                backup.put(threadLocal, threadLocal.get());
                // clear the TTL value only in captured
                if (!capturedMap.containsKey(threadLocal)) {
                    iterator.remove();
                    threadLocal.superRemove();
                }
            }
            // 把capture抓取的线程本地变量设置到当前线程的本地变量中
            for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : capturedMap.entrySet()) {
                @SuppressWarnings("unchecked")
                TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
                threadLocal.set(entry.getValue());
            }
            // call beforeExecute callback
            doExecuteCallback(true);
            return backup;
        }

restore

public static void restore(Object backup) {
            @SuppressWarnings("unchecked")
            Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup;
            // call afterExecute callback
            doExecuteCallback(false);

            for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
                 iterator.hasNext(); ) {
                Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
                TransmittableThreadLocal<?> threadLocal = next.getKey();

                // clear the TTL value only in backup
                // avoid the extra value of backup after restore
                if (!backupMap.containsKey(threadLocal)) {
                    iterator.remove();
                    threadLocal.superRemove();
                }
            }

            // 把replay步骤备份前`TTL`值重新设置回池化中线程的本地变量
            for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : backupMap.entrySet()) {
                @SuppressWarnings("unchecked")
                TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
                threadLocal.set(entry.getValue());
            }
        }

业务无感知
如果想让TTL框架更通用,对已经的业务场景进行无缝支持。需要把对Runnable、Callable、ExecuteService等池化类的装饰操作借助字节码在JVM启动时自动完成。
使用Java Agent来修饰JDK线程池实现类。这种方式实现线程池的传递是透明的,代码中没有修饰Runnable或是线程池的代码。即可以做到应用代码 无侵入
关于 无侵入 的更多说明参见文档Java Agent方式对应用代码无侵入。后面会写专门的文章介绍字节码增强这块的技术,这里就不展开了。

参考资料

JDK源码
https://github.com/alibaba/transmittable-thread-local#21-%E4%BF%AE%E9%A5%B0runnable%E5%92%8Ccallable

相关文章
|
安全 IDE Java
MapStruct - 原理讲解
MapStruct - 原理讲解
1661 2
MapStruct - 原理讲解
|
NoSQL Redis Docker
Mac下Docker安装Redis
Mac下Docker安装Redis
969 0
|
移动开发 算法 调度
【贪心算法】一文让你学会“贪心”(贪心算法详解及经典案例)
贪心算法是一种非常常见的算法,它的简单和高效性使其在实际应用中被广泛使用。 贪心算法的核心思想是在每一步都采取当前状态下最优的选择,而不考虑未来可能产生的影响。虽然贪心算法不能保证总是得到最优解,但在很多情况下,它可以获得很好的结果。 本篇文章将介绍贪心算法的基本概念和一些经典应用,以及如何通过贪心算法来解决一些实际问题。希望通过本文的阅读,读者可以对贪心算法有更加深刻的理解,并能够在实际问题中应用贪心算法来得到更好的解决方案。 让我们暴打贪心算法吧!
6685 0
|
存储 弹性计算 监控
【阿里云弹性计算】阿里云 ECS 性能优化秘籍:提升应用响应速度与资源利用率
【5月更文挑战第22天】阿里云ECS优化涉及实例规格选择、OS与应用配置、网络配置、存储优化及数据库连接池管理。合理挑选CPU和内存,关闭无关服务,利用EIP和负载均衡优化网络,选择合适存储类型,并通过监控工具进行性能分析和压力测试,以提升响应速度,优化资源利用率,降低成本,增强企业竞争力。示例展示了Java数据库连接池配置优化。通过持续探索和实践,可最大化发挥ECS潜力。
601 7
|
8月前
|
机器学习/深度学习 人工智能 API
AI 发展 && MCP
AI发展——计算机视觉、ChatGPT、Sora、DeepSeek、生成式AI。什么是MCP,Prompt、LLM、Function Call、Agent、MCP是什么,各自区别;MCP如何工作,MCP架构、MCP Server工作原理,Cursor如何使用MCP,自定义MCP Server
1245 46
|
SQL XML Java
八、(了解即可)MyBatis懒加载(或者叫延迟加载)
八、(了解即可)MyBatis懒加载(或者叫延迟加载)
392 1
|
分布式计算 Oracle 关系型数据库
实时计算 Flink版产品使用问题之获取Oracle的数据时无法获取clob类型的数据,该怎么办
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
小程序 项目管理 计算机视觉
软考高项备考经验分享
### 高级信息系统项目管理师备考经验概要 - 备考时间:建议3-6个月,每天2小时,周末4-6小时。 - 资料:用《信息系统项目管理师教程(第4版)》,配合视频课程(机构、淘宝、B站)。 - 综合理论:重点标记,多做题(如51CTO、希赛网、软考信管网题库),整理错题,记住常用英文词汇。 - 案例分析:重视计算,牢记公式,例如成本偏差(CV)、进度偏差(SV)等,多做练习。 - 论文写作:提前准备,理解10大管理过程,避免模板,利用真实项目或政府招标案例。 备考策略注重效率和针对性,论文准备尤为关键,案例和综合题型固定,需多练题掌握公式。
902 2
软考高项备考经验分享
|
前端开发
基于Camunda的开源项目
基于Camunda实现基本功能以及中国式审批流程相关功能
1086 0
基于Camunda的开源项目
|
弹性计算 Shell Linux
Docker 中 Gitlab 数据的备份和迁移
备份gitlab数据,并进行迁移恢复
3867 0
Docker 中 Gitlab 数据的备份和迁移