ThreadLocal 使用手册 | 建议收藏

2022-12-29  本文已影响0人  在中国喝Java

一、背景
为了使 Java 中的变量值在任何给定时间点跨不同线程可用,开发人员必须使用 Java 编程语言提供的同步机制,例如synchronized关键字或锁定对象。
这可确保任何时候只有一个线程获得访问权限,从而确保在可能存在争用问题的区域中使用变量时,多个线程的并发访问之间不会发生冲突。输入ThreadLocal。
Java 中的ThreadLocal类允许程序员创建只能由创建它们的线程访问的变量。这对于创建线程安全代码很有用,因为它确保每个线程都有自己的变量副本并且不会干扰其他线程。
这意味着在您的应用程序中运行的每个线程都将拥有自己的变量副本,具体取决于它们所属的上下文。在本编程教程中,我们将了解与ThreadLocal类相关的基本概念、它的优点、它的工作原理以及如何在 Java 应用程序中使用它。
二、Java 中的线程安全
在 Java 中实现线程安全的方法有很多种,每种方法都有其优缺点:

Synchronized:这是线程安全的最基本形式,在某些情况下可以有效。但是,如果不小心使用,它也会导致性能问题。
原子变量:这些变量可以原子方式读取和写入,无需同步。您可以利用 Java 中的 ThreadLocal 来降低同步成本。
不可变对象:如果一个对象的状态一旦创建就不能改变,则称它是不可变的。这通常与其他方法一起使用,例如同步方法或原子变量。
锁定对象:您可以利用这些对象来锁定一段代码,这样在特定点上只允许一个线程访问这段代码。与同步块或方法相比,它们可以实现更细粒度的控制,但也可能导致更复杂的代码。

三、Java 中的 ThreadLocal 是什么?
ThreadLocal是 Java 中的一个特殊类,它通过提供每线程上下文并为每个线程单独维护它们来帮助我们实现线程安全。换句话说,ThreadLocal是一个 Java 类,可用于定义只能由创建它们的线程访问的变量。这在许多情况下都很有用,但最常见的用例是当您需要存储不应在线程之间共享的数据时。
例如,假设开发人员正在编写一个多线程应用程序,每个线程都需要有自己的变量副本。如果您只是简单地使用一个常规变量,一个线程可能会在另一个线程有机会使用它之前覆盖该变量的值。使用ThreadLocal,每个线程都有自己的变量副本,因此不存在一个线程在另一个线程有机会使用它之前覆盖该值的风险。
ThreadLocal实例在需要存储线程特定信息的 Java 类中表示为私有静态字段。 ThreadLocal变量不是全局变量,因此除非显式传递给其他线程,否则其他线程无法访问它们。这使得它们非常适合存储敏感信息,例如密码或用户 ID,其他线程不应访问这些信息。
3.1 什么时候使用 ThreadLocal?
在 Java 中使用ThreadLocal有几个原因。最常见的用例是当您需要维护给定线程的状态信息时,但该状态不能在线程之间共享。例如,如果您使用 JDBC 连接池,每个线程都需要它的连接。在这种情况下,使用ThreadLocal允许每个线程都有自己的连接,而不必担心每次创建或销毁线程时创建和销毁连接的开销。
ThreadLocal的另一个常见用例是当您需要在单个线程中的不同组件之间共享状态信息时。例如,如果您有一个服务需要调用多个 DAO(数据库访问对象),每个 DAO 可能需要其ThreadLocal变量来存储当前事务或会话信息。这允许每个组件访问它需要的状态,而不用担心在组件之间传递数据。
最后,您还可以使用ThreadLocal作为为线程创建全局变量的简单方法。这对于调试或记录目的通常很有用。例如,您可以创建一个ThreadLocal变量来存储当前用户 ID。这将允许您轻松地记录该用户执行的所有操作,而不必到处传递用户 ID。
四、ThreadLocal 基础用法
4.1 创建一个 ThreadLocal
您创建ThreadLocal实例就像创建任何其他 Java 对象一样 - 通过new 运算符。这是一个显示如何创建ThreadLocal变量的示例:
private ThreadLocal threadLocal = new ThreadLocal();
复制代码
每个线程只需要执行一次。多个线程现在可以在 this ThreadLocal中获取和设置值,每个线程只能看到它自己设置的值。
4.2 设置 ThreadLocal 值
创建 a 后,您可以使用其方法 ThreadLocal设置要存储在其中的值 。set()
threadLocal.set("一个线程本地值");
复制代码
4.3 获取 ThreadLocal 值
ThreadLocal您使用其get()方法 读取存储在 a 中的值。这是获取存储在 Java 中的值的示例ThreadLocal:
String threadLocalValue = (String) threadLocal.get();
复制代码
4.4 删除 ThreadLocal 值
可以删除在 ThreadLocal 变量中设置的值。您可以通过调用该 ThreadLocal remove()方法来删除一个值。以下是删除 Java 上设置的值的示例ThreadLocal:
threadLocal.remove();
复制代码
4.5 删除所有ThreadLocal变量的值
最后,您可以调用clear() 方法来删除所有ThreadLocal变量的值。这通常仅在开发人员的程序关闭时才需要。例如,要清除所有ThreadLocal变量,可以使用以下代码:
threadLocal.clear();
复制代码
重要的是要注意ThreadLocal实例中的数据只能由创建它的线程访问。如果您尝试从另一个线程访问数据,您将得到一个IllegalStateException。
五、ThreadLocal 高级用法
5.1 泛型 ThreadLocal
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将在第一次 get()调用时使用 - 在set()使用新值调用之前。您有两个选项来指定 ThreadLocal 的初始值:

创建一个覆盖该initialValue()方法的 ThreadLocal 子类。
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首先使用 a 的全部意义在于避免不同的线程看到相同的实例。
2)Supplier 实现
为 Java 变量指定初始值的第二种方法ThreadLocal是使用其静态工厂方法withInitial(Supplier)将Supplier接口实现作为参数传递。此Supplier实现为 提供初始值 ThreadLocal。下面是一个ThreadLocal使用其 withInitial()静态工厂方法创建的示例,将一个简单的Supplier实现作为参数传递:
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
@Override
public String get() {
return String.valueOf(System.currentTimeMillis());
}
});
复制代码
由于Supplier是 功能接口,因此可以使用 Java Lambda 表达式来实现。以下是如何将Supplier实现作为 lambda 表达式提供给withInitial() looks:
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 实例。如果SimpleDateFormat尚未在 中设置实例ThreadLocal,则会创建一个新 实例SimpleDateFormat并将其设置在ThreadLocal变量中。一旦一个线程在变量中设置了它自己SimpleDateFormat的ThreadLocal变量,相同的SimpleDateFormat 对象将用于该线程继续前进。但仅限于该线程。每个线程都创建自己的SimpleDateFormat 实例,因为它们看不到彼此在ThreadLocal变量上设置的实例。
该类SimpleDateFormat不是线程安全的,因此多个线程不能同时使用它。为了解决这个问题,MyDateFormatter上面的类创建了一个SimpleDateFormat per thread,所以调用该format()方法的每个线程都将使用自己的SimpleDateFormat 实例。
5.4 Inheritable ThreadLocal
该类InheritableThreadLocal是 的子类ThreadLocal。不是每个线程在 a 中都有自己的值,而是ThreadLocal将对InheritableThreadLocal值的访问权限授予一个线程和该线程创建的所有子线程。这是一个完整的 JavaInheritableThreadLocal 示例:
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。然后该示例创建一个线程来设置 ThreadLocal 和 InheritableThreadLocal 的值 - 然后创建一个子线程来访问 ThreadLocal 和 InheritableThreadLocal 的值。只有 InheritableThreadLocal 的值对子线程可见。
最后,该示例创建了第三个线程,该线程也尝试访问 ThreadLocal 和 InheritableThreadLocal,但看不到第一个线程存储的任何值。
运行此示例打印的输出如下所示:
===== 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,它允许开发人员存储和检索特定于给定Thread的数据。换句话说,ThreadLocal允许您定义只能由创建它们的线程访问的变量。
如果使用得当,ThreadLocal可以成为创建高性能、线程安全代码的宝贵工具。但是,在您的 Java 应用程序中使用ThreadLocal之前,了解使用 ThreadLocal 的潜在风险和缺点很重要。
八、最后说一句
我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。

上一篇下一篇

猜你喜欢

热点阅读