ThreadLocal的使用及原理解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: JDK的lang包下提供了ThreadLocal类,我们可以使用它创建一个线程变量,线程变量的作用域仅在于此线程内

基本使用

JDK的lang包下提供了ThreadLocal类,我们可以使用它创建一个线程变量,线程变量的作用域仅在于此线程内。
用2个示例来展示一下ThreadLocal的用法。

示例一:

ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

System.out.println(threadLocal.get());
threadLocal.set(1);
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());

输出:

null
1
null

这个示例展示了ThreadLocal提供的所有方法,ThreadLocal中提供了三个方法,分别是:

  • get:获取变量值
  • set:设置变量值
  • remove:删除变量值

示例二:

//    创建一个MyRun类
class MyRun implements Runnable {

    //    创建2个线程变量,var1、var2
    private ThreadLocal<Integer> var1 = new ThreadLocal<>();
    private ThreadLocal<String> var2 = new ThreadLocal<>();

    @Override
    public void run() {
        //    循环调用m方法5次
        for (int i = 0; i < 5; i++) {
            m();
        }
    }

    public void m() {
        //    当前线程名称
        String name = Thread.currentThread().getName();

        //    var1变量从1开始,m每次调用递增1
        Integer v = var1.get();
        if(v == null) {
            var1.set(1);
        }else {
            var1.set(v + 1);
        }

        //    var2变量 = 线程名 - var1值
        var2.set(name + "-" + var1.get());

        //    打印
        print();
    }

    public void print() {
        String name = Thread.currentThread().getName();
        System.out.println(name + ", var1: " + var1.get() + ", var2: " + var2.get());
    }
}

创建2个线程,执行同一个MyRun:

MyRun myRun = new MyRun();
Thread t1 = new Thread(myRun);
Thread t2 = new Thread(myRun);
t1.start();
t2.start();

输出:

Thread-0, var1: 1, var2: Thread-0-1
Thread-1, var1: 1, var2: Thread-1-1
Thread-0, var1: 2, var2: Thread-0-2
Thread-1, var1: 2, var2: Thread-1-2
Thread-0, var1: 3, var2: Thread-0-3
Thread-1, var1: 3, var2: Thread-1-3
Thread-0, var1: 4, var2: Thread-0-4
Thread-0, var1: 5, var2: Thread-0-5
Thread-1, var1: 4, var2: Thread-1-4
Thread-1, var1: 5, var2: Thread-1-5

示例二展示了ThreadLocal的重要特点:
两个线程执行的是同一个MyRun对象,如果var1、var2是普通的成员变量,两个线程访问的将是同一个变量,这将会产生线程安全问题,然而从输出日志看来,t1、t2的var1、var2值其实是独立的,互不影响的。

这是因为var1、var2是ThreadLocal类型,即是线程变量,它是绑定在线程上的,哪个线程来访问这段代码,就从哪个线程上获取var1、var2变量值,线程与线程之间是相互隔离的,因此也不存在线程安全问题。

原理解析

ThreadLocal是如何实现这个效果的呢?
我们可以从ThreadLocal的源代码中一探究竟。

其中,最关键是get方法,我将get相关的源代码都提取出来如下:

public T get() {
    //    获取当前线程对象
    Thread t = Thread.currentThread();
    //    从当前线程中获取ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    
    if (map != null) {
        //    从ThreadLocalMap对象中获取当前ThreadLocal对应Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            //    若Entry不为null,返回值
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    
    //    如果获取ThreadLocalMap对象为null则返回默认值
    return setInitialValue();
}

//    从指定线程对象获取ThreadLocalMap,也就是Thread中的threadLocals
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//    默认值
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    
    if (map != null)
        map.set(this, value);//      如果当前线程的threadLocals不为null,则赋默认值
    else
        createMap(t, value);  //    如果当前线程的threadLocals为null,则新建
    return value;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

protected T initialValue() {
    return null;  //  初始值是null
}

从以上这段代码可以看出,ThreadLocal访问的实际上是当前线程的成员变量threadLocals。
threadLocals的数据类型是ThreadLocalMap,这是JDK中专门为ThreadLocal设计的数据结构,它本质就是一个键值对类型。
ThreadLocalMap的键存储的是当前ThreadLocal对象,值是ThreadLocal对象实际存储的值。
当用ThreadLocal对象get方法时,它实际上是从当前线程的threadLocals获取键为当前ThreadLocal对象所对应的值。

画张图来辅助一下理解:

清楚了ThreadLocal的get原理,set和remove方法不需要看源码也能猜出是怎么写的。
无非是以ThreadLocal对象为键设置其值或删除键值对。

ThreadLocal的初始值

上面的介绍,我们看到ThreadLocal的initialValue方法永远都是返回null的:

protected T initialValue() {
    return null;  //  初始值是null
}

如果想要设定ThreadLocal对象的初始值,可以用以下方法:

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->1);
System.out.println(threadLocal.get());

withInitial方法内实际返回的是一个ThreadLocal子类SuppliedThreadLocal对象。
SuppliedThreadLocal重写了ThreadLocal的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() {
        return supplier.get();
    }
}

获取父线程的ThreadLocal变量

在一些场景下,我们可能需要子线程能获取到父线程的ThreadLocal变量,但使用ThreadLocal是无法获取到的:

public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {
    threadLocal.set(1);
    System.out.println(threadLocal.get());

    Thread childThread = new Thread(() -> System.out.println(threadLocal.get()));
    childThread.start();
}

输出:

1
null

使用ThreadLocal的子类InheritableThreadLocal可以达到这个效果:

public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {
    threadLocal.set(1);
    System.out.println(threadLocal.get());

    Thread childThread = new Thread(() -> System.out.println(threadLocal.get()));
    childThread.start();
}
1
1

InheritableThreadLocal是怎么做到的呢?

我们来分析一下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的三个方法childValue、getMap、createMap。
childValue方法用于ThreadLocalMap内部使用,我们不打算讲解ThreadLocalMap内部设计,这里可以忽略;
ThreadLocal本来getMap、createMap读写的是当前Thread对象的threadLocals变量。
而InheritableThreadLocal将其改为了读写当前Thread对象的InheritableThreadLocal变量。

接着我们要从Thread类的源码查找头绪。

Thread类源代码中,我们可以看到有这么2个成员变量:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

如果是使用ThreadLocal创建线程变量,读写的是Thread对象的threadLocals;
如果是使用InheritableThreadLocal创建线程变量,读写的是Thread对象的inheritableThreadLocals。

在Thread类的init方法可以看到(Thread所有构造方法都是调用init方法,这边仅贴出关键部分):

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

ThreadLocal.createInheritedMap:

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

如果父级线程的inheritableThreadLocals不为null,那么将父级线程的inheritableThreadLocals赋值到当前线程的inheritableThreadLocals变量。

总结:当使用InheritableThreadLocal创建线程变量时,父线程读写线程变量实际是写入父线程的inheritableThreadLocals中,在创建子线程时,会将父线程的inheritableThreadLocals复制给子线程的inheritableThreadLocals,子线程操作此线程变量时,也是读写自己线程的inheritableThreadLocals,这就达到了子线程可以获取父线程ThreadLocal的效果。

其他要点

  • 如果使用了线程池,线程是会被复用的,因此线程的threadLocals和inheritableThreadLocals也会复用,在线程池使用ThreadLocal可能会产生一些问题,需要留意;
  • JDK本身提供创建线程池的方法,是不支持获得父级线程的ThreadLocal变量的。
目录
相关文章
|
1月前
|
存储 缓存 算法
HashMap深度解析:从原理到实战
HashMap,作为Java集合框架中的一个核心组件,以其高效的键值对存储和检索机制,在软件开发中扮演着举足轻重的角色。作为一名资深的AI工程师,深入理解HashMap的原理、历史、业务场景以及实战应用,对于提升数据处理和算法实现的效率至关重要。本文将通过手绘结构图、流程图,结合Java代码示例,全方位解析HashMap,帮助读者从理论到实践全面掌握这一关键技术。
88 13
|
2月前
|
运维 持续交付 云计算
深入解析云计算中的微服务架构:原理、优势与实践
深入解析云计算中的微服务架构:原理、优势与实践
97 1
|
8天前
|
机器学习/深度学习 自然语言处理 搜索推荐
自注意力机制全解析:从原理到计算细节,一文尽览!
自注意力机制(Self-Attention)最早可追溯至20世纪70年代的神经网络研究,但直到2017年Google Brain团队提出Transformer架构后才广泛应用于深度学习。它通过计算序列内部元素间的相关性,捕捉复杂依赖关系,并支持并行化训练,显著提升了处理长文本和序列数据的能力。相比传统的RNN、LSTM和GRU,自注意力机制在自然语言处理(NLP)、计算机视觉、语音识别及推荐系统等领域展现出卓越性能。其核心步骤包括生成查询(Q)、键(K)和值(V)向量,计算缩放点积注意力得分,应用Softmax归一化,以及加权求和生成输出。自注意力机制提高了模型的表达能力,带来了更精准的服务。
|
19天前
|
存储 物联网 大数据
探索阿里云 Flink 物化表:原理、优势与应用场景全解析
阿里云Flink的物化表是流批一体化平台中的关键特性,支持低延迟实时更新、灵活查询性能、无缝流批处理和高容错性。它广泛应用于电商、物联网和金融等领域,助力企业高效处理实时数据,提升业务决策能力。实践案例表明,物化表显著提高了交易欺诈损失率的控制和信贷审批效率,推动企业在数字化转型中取得竞争优势。
75 14
|
28天前
|
网络协议 安全 网络安全
探索网络模型与协议:从OSI到HTTPs的原理解析
OSI七层网络模型和TCP/IP四层模型是理解和设计计算机网络的框架。OSI模型包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,而TCP/IP模型则简化为链路层、网络层、传输层和 HTTPS协议基于HTTP并通过TLS/SSL加密数据,确保安全传输。其连接过程涉及TCP三次握手、SSL证书验证、对称密钥交换等步骤,以保障通信的安全性和完整性。数字信封技术使用非对称加密和数字证书确保数据的机密性和身份认证。 浏览器通过Https访问网站的过程包括输入网址、DNS解析、建立TCP连接、发送HTTPS请求、接收响应、验证证书和解析网页内容等步骤,确保用户与服务器之间的安全通信。
102 1
|
2月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
101 17
|
2月前
|
运维 持续交付 虚拟化
深入解析Docker容器化技术的核心原理
深入解析Docker容器化技术的核心原理
62 1
|
2月前
|
存储 供应链 算法
深入解析区块链技术的核心原理与应用前景
深入解析区块链技术的核心原理与应用前景
80 0
|
2月前
|
API 持续交付 网络架构
深入解析微服务架构:原理、优势与实践
深入解析微服务架构:原理、优势与实践
56 0
|
2月前
|
存储 供应链 物联网
深入解析区块链技术的核心原理与应用前景
深入解析区块链技术的核心原理与应用前景

热门文章

最新文章

推荐镜像

更多