ThreadLocal的使用及原理解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 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变量的。
目录
相关文章
|
7天前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
18天前
|
域名解析 网络协议
DNS服务工作原理
文章详细介绍了DNS服务的工作原理,包括FQDN的概念、名称解析过程、DNS域名分级策略、根服务器的作用、DNS解析流程中的递归查询和迭代查询,以及为何有时基于IP能访问而基于域名不能访问的原因。
38 2
|
14天前
|
负载均衡 网络协议 安全
DNS解析中的Anycast技术:原理与优势
【9月更文挑战第7天】在互联网体系中,域名系统(DNS)将域名转换为IP地址,但网络规模的扩张使DNS解析面临高效、稳定与安全挑战。Anycast技术应运而生,通过将同一IP地址分配给多个地理分布的服务器,并依据网络状况自动选择最近且负载低的服务器响应查询请求,提升了DNS解析速度与效率,实现负载均衡,缓解DDoS攻击,增强系统高可用性。此技术利用动态路由协议如BGP实现,未来在网络发展中将扮演重要角色。
43 0
|
21天前
|
开发者 安全 UED
JSF事件监听器:解锁动态界面的秘密武器,你真的知道如何驾驭它吗?
【8月更文挑战第31天】在构建动态用户界面时,事件监听器是实现组件间通信和响应用户操作的关键机制。JavaServer Faces (JSF) 提供了完整的事件模型,通过自定义事件监听器扩展组件行为。本文详细介绍如何在 JSF 应用中创建和使用事件监听器,提升应用的交互性和响应能力。
18 0
|
21天前
|
前端开发 Java UED
瞬间变身高手!JSF 与 Ajax 强强联手,打造极致用户体验的富客户端应用,让你的应用焕然一新!
【8月更文挑战第31天】JavaServer Faces (JSF) 是 Java EE 标准的一部分,常用于构建企业级 Web 应用。传统 JSF 应用采用全页面刷新方式,可能影响用户体验。通过集成 Ajax 技术,可以显著提升应用的响应速度和交互性。本文详细介绍如何在 JSF 应用中使用 Ajax 构建富客户端应用,并通过具体示例展示 Ajax 在 JSF 中的应用。首先,确保安装 JDK 和支持 Java EE 的应用服务器(如 Apache Tomcat 或 WildFly)。
27 0
|
21天前
|
Java Spring
🔥JSF 与 Spring 强强联手:打造高效、灵活的 Web 应用新标杆!💪 你还不知道吗?
【8月更文挑战第31天】JavaServer Faces(JSF)与 Spring 框架是常用的 Java Web 技术。本文介绍如何整合两者,发挥各自优势,构建高效灵活的 Web 应用。首先通过 `web.xml` 和 `ContextLoaderListener` 配置 Spring 上下文,在 `applicationContext.xml` 定义 Bean。接着使用 `@Autowired` 将 Spring 管理的 Bean 注入到 JSF 管理的 Bean 中。
31 0
|
21天前
|
监控 数据库 开发者
云端飞跃:Play Framework应用的惊心动魄部署之旅,从本地到云的华丽转身
【8月更文挑战第31天】Play Framework是一款高效Java和Scala Web应用框架,支持快速开发与灵活部署。本文详细介绍从本地环境到云平台(如Heroku和AWS Elastic Beanstalk)的部署策略,涵盖配置文件设置、依赖管理和环境变量配置等关键步骤,并提供示例代码,帮助开发者顺利完成部署。此外,还介绍了如何进行日志和性能监控,确保应用稳定运行。通过本文,开发者可充分利用云计算的优势,实现高效部署与维护。
24 0
|
21天前
|
SQL 监控 数据库
深度解析Entity Framework Core中的变更跟踪与并发控制:从原理到实践的全方位指南,助你构建稳健高效的数据访问层
【8月更文挑战第31天】本文通过问答形式深入探讨了 Entity Framework Core 中的变更跟踪与并发控制。变更跟踪帮助我们监控实体状态变化,默认适用于所有实体,但可通过特定配置关闭。并发控制确保多用户环境下数据的一致性,包括乐观和悲观两种方式。文章提供了具体代码示例,展示了如何配置和处理相关问题,帮助读者在实际项目中更高效地应用这些技术。
27 0
|
21天前
|
JavaScript 前端开发 开发者
深入解析Angular装饰器:揭秘框架核心机制与应用——从基础用法到内部原理的全面教程
【8月更文挑战第31天】本文深入解析了Angular框架中的装饰器特性,包括其基本概念、使用方法及内部机制。装饰器作为TypeScript的关键特性,在Angular中用于定义组件、服务等。通过具体示例介绍了`@Component`和`@Injectable`装饰器的应用,展示了如何利用装饰器优化代码结构与依赖注入,帮助开发者构建高效、可维护的应用。
21 0
|
21天前
|
缓存 JavaScript 前端开发
【React生态进阶】React与Redux完美结合:从原理到实践全面解析构建大规模应用的最佳策略与技巧分享!
【8月更文挑战第31天】React 与 Redux 的结合解决了复杂状态管理的问题,提升了应用性能。本文详细介绍了在 React 应用中引入 Redux 的原因、步骤及最佳实践,包括安装配置、状态管理、性能优化等多方面内容,并提供了代码示例,帮助你构建高性能、易维护的大规模应用。
26 0

推荐镜像

更多