我是石页兄,朋友不因远而疏,高山不隔友谊情;偶遇美羊羊,我们互相鼓励欢迎关注微信公众号「架构染色」交流和学习
一、背景
为了使 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 值
使用ThreadLocal
的get()
方法读取存储在其中的值。
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
。然后,这个例子创建了一个线程来设置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,它允许开发人员存储和检索特定于某个线程的数据。换句话说,ThreadLocal
允许你定义只有创建这些变量的线程才能访问的变量。
如果使用得当,ThreadLocal
可以成为创建高性能、线程安全的代码的宝贵工具。然而,在你的 Java 应用程序中使用ThreadLocal
之前,必须意识到使用它的潜在风险和弊端。
八、最后说一句
我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。