开启生长之旅!这是我参加「日新方案 12 月更文应战」的第 36 天,点击检查活动详情

我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,咱们相互鼓励

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

ThreadLocal 使用手册 | 建议收藏

一、背景

为了使 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();

每个线程只需求履行一次。多个线程现在能够在 thisThreadLocal中获取和设置值,每个线程只能看到它自己设置的值。

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 变量中设置的值。您能够经过调用该 ThreadLocalremove()办法来删去一个值。以下是删去 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变量中。一旦一个线程在变量中设置了它自己SimpleDateFormatThreadLocal变量,相同的SimpleDateFormat 目标将用于该线程继续前进。但仅限于该线程。每个线程都创立自己的SimpleDateFormat 实例,由于它们看不到互相在ThreadLocal变量上设置的实例。

该类SimpleDateFormat不是线程安全的,因而多个线程不能同时运用它。为了处理这个问题,MyDateFormatter上面的类创立了一个SimpleDateFormatper 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。然后该示例创立一个线程来设置 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,它答应开发人员存储和检索特定于给定Thread的数据。换句话说,ThreadLocal答应您定义只能由创立它们的线程拜访的变量。

假如运用妥当,ThreadLocal能够成为创立高功能、线程安全代码的名贵东西。可是,在您的 Java 应用程序中运用ThreadLocal之前,了解运用 ThreadLocal 的潜在危险和缺陷很重要。

八、最终说一句

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

欢迎点击链接扫马儿重视、交流。

翻译原文

  • jenkov.com/tutorials/j…
  • www.developer.com/java/java-t…