ThreadLocal 使用手册

简介: Java 中的`ThreadLocal`类允许程序员创建只能由创建它们的线程访问的变量。这对于创建线程安全代码很有用,因为它确保每个线程都有自己的变量副本并且不会干扰其他线程。
我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,我们互相鼓励

欢迎关注微信公众号「架构染色」交流和学习

一、背景

为了使 Java 中的一个变量的值在任何给定的时间点上都能跨越不同的线程,开发人员必须使用 Java 编程语言提供的同步机制,如同步关键字或锁定对象。

这可以确保在任何时候只有一个线程获得访问权,确保在使用那些有可能出现争用问题的区域内的变量时,多个线程的并发访问不会产生冲突。进入ThreadLocal

Java 中的ThreadLocal类允许程序员创建只有创建这些变量的线程才能访问的变量。这对于创建线程安全的代码很有用,因为它确保每个线程都有自己的变量副本,并且不能干扰其他线程。

这意味着在你的应用程序中运行的每个线程都会有自己的变量副本。在这个编程教程中,我们将了解与ThreadLocal类相关的基本概念,它的好处,它的工作原理,以及如何在 Java 应用程序中使用它。

二、Java 中的线程安全

在 Java 中实现线程安全的方法有很多种,每种方法都有其优缺点:

  • Synchronized 代码块或方法。这是最基本的线程安全形式,在某些情况下它是有效的。然而,如果不小心使用,它也会导致性能问题。
  • 原子变量。这些是可以原子方式读写的变量,不需要同步。你可以利用 Java 中的 ThreadLocal 来减少同步化的成本。
  • 不可变的对象。如果一个对象的状态一旦创建就不能改变,那么它就被称为不可变的。这通常与其他方法一起使用,如同步方法或原子变量。
  • 锁对象。你可以利用这些对象来锁定一大块代码,从而使这块代码在某一特定时刻只允许被一个线程访问。与同步代码块或方法相比,它们能够实现更好的细粒度控制,但也可能导致更复杂的代码。

在 Java 中实现线程安全的方法有很多,每种方法都有其优点和缺点。

三、Java 中的 ThreadLocal 是什么?

ThreadLocal是 Java 中的一个特殊类,它通过提供每个线程的上下文并为每个线程单独维护它们来帮助我们实现线程安全。换句话说,ThreadLocal是一个 Java 类,可以用来定义只由创建它们的线程访问的变量。这在很多情况下都很有用,但最常见的使用情况是,你需要存储不在线程之间共享的数据。

例如,假设一个开发者正在编写一个多线程的应用程序,每个线程需要有自己的变量副本。如果你只是简单地使用一个普通的变量,有可能一个线程会在另一个线程有机会使用它之前就覆盖了该变量的值。有了ThreadLocal,每个线程都有自己的变量副本,所以不存在一个线程在另一个线程有机会使用它之前就覆盖了该值的风险。

一个ThreadLocal实例在需要存储线程特定信息的 Java 类中被表示为一个私有静态字段。ThreadLocal变量不是全局变量,所以它们不能被其他线程访问,除非它们被明确传递给其他线程。这使得它们成为存储敏感信息的理想选择,如密码或用户 ID,它们不应该被其他线程访问。

3.1 什么时候使用 ThreadLocal?

在 Java 中使用ThreadLocal有几个原因。最常见的用例是当你需要为一个给定的线程维护状态信息,但该状态在线程之间是不可共享的。例如,如果你使用一个 JDBC 连接池,每个线程都需要它的连接。在这种情况下,使用ThreadLocal允许每个线程拥有自己的连接,而不必担心每次创建或销毁线程时创建和销毁连接的开销。

ThreadLocal的另一个常见用例是当你需要在一个线程中的不同组件之间共享状态信息时。例如,如果你有一个服务需要调用多个 DAO(数据库访问对象),每个 DAO 可能需要其ThreadLocal变量来存储当前的事务或会话信息。允许每个组件通过ThreadLocal访问它所需要的状态,而不必担心组件之间的数据传递。

最后,你也可以使用ThreadLocal作为一个简单的方法来为一个线程创建全局变量。这对于调试或记录的场景通常是有用的。例如,你可以创建一个ThreadLocal变量来存储当前的用户 ID。你将轻松地记录该用户执行的所有操作,而不必到处传递用户 ID。

四、ThreadLocal 基础用法

4.1 创建一个 ThreadLocal

创建ThreadLocal实例就像创建任何其他 Java 对象一样 - 通过new  运算符。

private ThreadLocal threadLocal = new ThreadLocal();

这每个线程中只需要做一次。多个线程可以在这个 ThreadLocal 中获取和设置值,而每个线程将只看到它自己设置的值。

4.2 设置 ThreadLocal 值

一旦一个ThreadLocal被创建,你可以使用它的set()方法来设置要存储在其中的值。

threadLocal.set("一个线程本地值");

4.3 获取 ThreadLocal 值

使用ThreadLocalget()方法读取存储在其中的值。

String threadLocalValue = (String) threadLocal.get();

4.4 删除 ThreadLocal 值

可以删除在 ThreadLocal 变量中设置的值。可以通过调用remove()方法来删除一个值。

threadLocal.remove();

4.5 删除所有ThreadLocal变量的值

最后,您可以调用clear() 方法来删除所有ThreadLocal变量的值。这通常仅在开发人员的程序关闭时才需要。例如,要清除所有ThreadLocal变量,可以使用以下代码:

threadLocal.clear();

注意:原文中描述的此方法,在 JDK8\17\18 中其实均未找到,读者老师支付宝小程序团队也有留言提出此疑惑,希望了解情况的读者老师烦请留言解惑。 关于如何清理的问题,在 stackoverflow 中有看到一些有意思的方案讨论how-to-clean-up-threadlocals,后续会翻译整理出来。

小结

重要的是要注意ThreadLocal实例中的数据只能由创建它的线程访问。

五、ThreadLocal 高级用法

5.1 泛型 ThreadLocal

您可以使用泛化类型创建一个。使用泛型类型只能将泛型类型的对象设置为ThreadLocal的值.  此外,你不需要对get()返回的值进行类型转换。下面是一个泛型 ThreadLocal 的例子。

private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();

现在您只能在ThreadLocal实例中存储字符串。此外,你不需要对从ThreadLocal获得的值进行类型转换:

myThreadLocal.set("Hello ThreadLocal");

String threadLocalValue = myThreadLocal.get();

5.2 初始 ThreadLocal 值

可以为一个 Java ThreadLocal设置一个初始值,除非被set()新的值,否则 get() 的总是这个初始值。你有两个选择来为 ThreadLocal 指定一个初始值。

  • 创建一个 ThreadLocal 子类,重写 initialValue()方法。
  • 创建一个具有Supplier接口实现的 ThreadLocal。
    我将在下面的章节中向你展示这两种选择。

1) Override initialValue()

为 Java ThreadLocal变量指定初始值的第一种方法是创建一个 ThreadLocal的子类,重写其initialValue()方法。创建ThreadLocal子类的最简单方法是简单地创建一个匿名子类,就在你创建ThreadLocal变量的地方。下面是一个创建ThreadLocal的匿名子类的例子,它覆盖了initialValue()方法。

private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return String.valueOf(System.currentTimeMillis());
    }
};

注意,不同的线程仍然会看到不同的初始值。每个线程将创建自己的初始值。只有当你从initialValue()方法中返回完全相同的对象时,所有线程才能看到相同的对象。然而,首先使用ThreadLocal的全部意义在于避免不同线程看到相同的实例。

2)Supplier 实现

为 Java ThreadLocal变量指定初始值的第二种方法是使用其静态工厂方法withInitial(Supplier),并将Supplier接口的实现作为参数传递给它。这个Supplier实现为ThreadLocal提供初始值。下面是一个使用其静态工厂方法withInitial()创建ThreadLocal的例子,其中传递了一个简单的Supplier实现作为参数。

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
    @Override
    public String get() {
        return String.valueOf(System.currentTimeMillis());
    }
});

由于Supplier是一个功能接口,它可以用 Java Lambda 表达式来实现。下面是将Supplier的实现作为一个 lambda 表达式提供给withInitial()的样子。

ThreadLocal threadLocal = ThreadLocal.withInitial(
        () -> { return String.valueOf(System.currentTimeMillis()); } );

正如你所看到的,这比前面的例子要短一些。但它还可以更短一些,使用最密集的 lambda 表达式的语法。

ThreadLocal threadLocal3 = ThreadLocal.withInitial(
        () -> String.valueOf(System.currentTimeMillis()) );

5.3 ThreadLocal 延迟初始化

在某些情况下,你不能使用设置初始值的标准方法。例如,也许你需要一些配置信息,而这些信息在你创建ThreadLocal变量时是不可用的。在这种情况下,你可以延迟地设置初始值。下面是一个例子,说明如何在 Java ThreadLocal上延迟设置初始值。

public class MyDateFormatter {

    private ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<>();

    public String format(Date date) {
        SimpleDateFormat simpleDateFormat = getThreadLocalSimpleDateFormat();
        return simpleDateFormat.format(date);
    }


    private SimpleDateFormat getThreadLocalSimpleDateFormat() {
        SimpleDateFormat simpleDateFormat = simpleDateFormatThreadLocal.get();
        if(simpleDateFormat == null) {
            simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            simpleDateFormatThreadLocal.set(simpleDateFormat);
        }
        return simpleDateFormat;
    }
}

注意format()方法是如何调用getThreadLocalSimpleDateFormat()方法来获得一个 Java SimpleDatFormat实例的。如果在ThreadLocal中没有设置SimpleDateFormat实例,就会创建一个新的SimpleDateFormat,并在ThreadLocal变量中设置。一旦一个线程在ThreadLocal变量中设置了自己的SimpleDateFormat,同一个SimpleDateFormat对象就会被用于该线程。但只适用于该线程。每个线程都会创建自己的SimpleDateFormat实例,因为它们不能看到彼此在ThreadLocal变量中设置的实例。

SimpleDateFormat类不是线程安全的,所以多个线程不能同时使用它。为了解决这个问题,上面的MyDateFormatter类为每个线程创建了一个SimpleDateFormat,所以每个调用format()方法的线程将使用它自己的SimpleDateFormat实例。

5.4 Inheritable ThreadLocal

InheritableThreadLocal类是ThreadLocal的一个子类。InheritableThreadLocal不是让每个线程在ThreadLocal中拥有自己的值,而是让一个线程和由该线程创建的所有子线程都能获得值。下面是一个完整的 Java InheritableThreadLocal例子。

public class InheritableThreadLocalBasicExample {

    public static void main(String[] args) {

        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        InheritableThreadLocal<String> inheritableThreadLocal =
                new InheritableThreadLocal<>();

        Thread thread1 = new Thread(() -> {
            System.out.println("===== Thread 1 =====");
            threadLocal.set("Thread 1 - ThreadLocal");
            inheritableThreadLocal.set("Thread 1 - InheritableThreadLocal");

            System.out.println(threadLocal.get());
            System.out.println(inheritableThreadLocal.get());

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

        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("===== Thread2 =====");
            System.out.println(threadLocal.get());
            System.out.println(inheritableThreadLocal.get());
        });
        thread2.start();
    }
}

这个例子创建了一个普通的 Java ThreadLocal和一个 Java InheritableThreadLocal。然后,这个例子创建了一个线程来设置ThreadLocalInheritableThreadLocal的值--然后创建一个子线程来访问ThreadLocalInheritableThreadLocal的值。只有InheritableThreadLocal的值对子线程是可见的。

最后,这个例子创建了第三个线程,它也试图访问ThreadLocalInheritableThreadLocal - 但它没有看到第一个线程存储的任何值。

运行这个例子的输出结果是这样的。

===== Thread 1 =====
Thread 1 - ThreadLocal
Thread 1 - InheritableThreadLocal
===== ChildThread =====
null
Thread 1 - InheritableThreadLocal
===== Thread2 =====
null
null

六、使用 Java 的 ThreadLocal 的优点和缺点

如果使用得当,Java 中的ThreadLocal类可以减少同步的开销并提高性能。通过消除内存泄漏,可以更轻松地阅读和维护代码。

当程序员需要维护特定于单个线程的状态时,当他们需要通过减少同步来提高性能时,以及当他们需要防止内存泄漏时,他们可以使用ThreadLocal变量。

与使用ThreadLocal变量相关的一些缺点包括竞争条件和内存泄漏。

如何防止竞争条件

ThreadLocal变量时,没有保证可以防止竞争条件的方法,因为它们天生就容易出现竞争条件。但是,有一些最佳实践可以帮助减少竞争条件的可能性,例如使用原子操作并确保对ThreadLocal变量的所有访问都正确同步。

七、关于 Java 中 ThreadLocal 的最终思考

ThreadLocal是 Java 中的一个强大的 API,它允许开发人员存储和检索特定于某个线程的数据。换句话说,ThreadLocal允许你定义只有创建这些变量的线程才能访问的变量。

如果使用得当,ThreadLocal可以成为创建高性能、线程安全的代码的宝贵工具。然而,在你的 Java 应用程序中使用ThreadLocal之前,必须意识到使用它的潜在风险和弊端。

八、最后说一句

我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。

欢迎点击链接扫马儿关注、交流。

翻译原文

相关文章
|
6月前
|
存储 Java 中间件
《吊打面试官系列》从源码全面解析 ThreadLocal 关键字的来龙去脉
《吊打面试官系列》从源码全面解析 ThreadLocal 关键字的来龙去脉
|
6月前
|
存储 安全 Java
面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
字节面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
78 0
|
4月前
|
存储 算法 Java
ThreadLocal 源码浅析
【7月更文挑战第5天】`ThreadLocal` 是Java中用于创建线程局部变量的工具类,确保每个线程拥有独立的变量副本。源码中,每个`ThreadLocal`实例都有一个唯一的哈希码用于映射到`ThreadLocalMap`,这个内部静态类使用弱引用存储键(`ThreadLocal`实例)以防止内存泄漏。`ThreadLocalMap`使用 Entry 数组,Entry 是一个扩展了 WeakReference 的类,持有线程变量值。`ThreadLocal`的`get()`和`set()`方法通过哈希计算定位并访问或设置线程局部变量。
|
5月前
|
存储 安全 Java
《ThreadLocal使用与学习总结:》史上最详细由浅入深解析ThreadLocal
《ThreadLocal使用与学习总结:》史上最详细由浅入深解析ThreadLocal
46 0
|
Java 应用服务中间件 Spring
ThreadLocal 的使用及踩坑
ThreadLocal 的使用及踩坑
91 1
|
存储 Java
大厂是怎么用ThreadLocal?ThreadLocal核心原理分析
ThreadLocal**是Java中的一个线程本地变量类。它可以让每个线程都有自己独立的变量副本,而不会相互影响。
119 1
|
Java
ThreadLocal笔记
ThreadLocal笔记
202 0
ThreadLocal笔记
|
存储 缓存 前端开发
不用session也能随时获取globalUser,用ThreadLocal也太香了
不用session也能随时获取globalUser,用ThreadLocal也太香了
|
存储 消息中间件 JavaScript
ThreadLocal 你真的用不上吗?
ThreadLocal 你真的用不上吗?
|
存储 安全 Java