一、前言
在Java中,线程本地变量的作用域是整个线程。这意味着这种变量可以从线程中的任何位置设置,并可以从同一线程的任何位置访问。从一个线程设置的值对另一个线程是不可访问的。
我们应该知道,Java中有两种类型的线程本地类——ThreadLocal和InheritableThreadLocal。让我们看看这两者之间的区别。
二、ThreadLocal类
下面是一个如何声明线程本地变量的示例。变量user是一个ThreadLocal变量,它保存一个User类型的变量(类或接口)。请注意,这里变量被声明为public和static,以便user变量可以从代码中的任何位置访问。
// 声明一个线程本地变量user
public static final ThreadLocal user
= new ThreadLocal<>();
下面是我们如何为一个线程设置和获取user。该示例显示user变量被设置为用户对象bob。在同一线程中,如果我们调用get()方法,就会检索到用户bob。
// 设置调用线程的user值
user.set(new User("bob"));
// 获取调用线程的user值
User requestUser = user.get();
请注意,即使user变量对整个代码库是可访问的,但set(..)方法确保传递给它的用户对象与calling线程相关联。get()方法也会检索与calling线程相关联的用户对象,这就是为什么当在不同线程上调用get()方法时,它不会检索到bob而是其他用户(或null)的原因。每个Java线程都与一个包含该线程所有设置的线程本地变量的ThreadLocal映射相关联。
如果我们在未设置任何值的情况下调用get()方法,该方法将简单地返回null。
然而,你可以创建一个带有Lambda Supplier的线程本地对象,它将返回一个初始的用户对象。下面的示例显示了一个Supplier,它返回一个名为anonymous的用户。因此,如果在未设置值的情况下调用ThreadLocal上的get()方法,则会调用Supplier上的get()方法,并将该值设置为用户的初始值。
// 声明一个带有Supplier的线程本地变量user
public static ThreadLocal user
= ThreadLocal.withInitial(
() -> new User("anonymous"))
// 返回Anonymous
User requestUser = user.get();
你也可以通过简单地调用remove()方法来删除之前设置的值,如下所示。
// 删除调用线程的user值
user.remove();
该方法基本上会删除与线程相关联的用户对象。更重要的是,其他线程不会受到此操作的影响。
如果我们以图表形式来可视化线程本地变量,它看起来会像这样。请注意,两个线程的user变量指向的是不同的用户对象。
图片
三、ThreadLocal和子线程
到目前为止,我们的讨论主要集中在单个Java线程上。如果一个Java线程启动了一个新的子线程,子线程会自动能够访问父线程中定义的线程本地变量吗?
答案是否定的!子线程无法访问父线程的线程本地变量,这是有充分理由的。如果能够访问,那么存储在线程本地变量中的对象就必须为线程安全而编写,因为多个线程能够访问同一个用户对象。这是Java工程师做出的一个很好的默认设计决策。
但是,在某些情况下,这种访问是有用的。想象一个Web应用程序的场景,许多用户正在访问应用程序。一个单独的Java线程与整个请求处理过程中的用户相关联,你可以想象用户对象存储在线程的线程本地对象中(这是许多应用服务器和框架如Spring Boot所做的)。但是,你可能希望生成的子线程也能访问这些用户信息。
对于这种场景,Java提供了另一个名为InheritableThreadLocal的类。
四、InheritableThreadLocal类
使用这个类的语法与ThreadLocal类基本相同。下面的示例显示了InheritableThreadLocal类的相应方法。
// 声明一个可继承的线程本地变量user
public static final InheritableThreadLocal user
= new InheritableThreadLocal<>();
// 设置调用线程的user值
user.set(new User("bob"));
// 获取调用线程的user值
User requestUser = user.get();
// 删除调用线程的user值
user.remove();
与Thread Local映射一样,每个线程也有一个用于可继承线程本地变量的映射。这里的关键区别是,当创建子线程时,子线程的可继承线程本地映射会从父线程克隆。因此,可继承线程本地变量对子线程也是可访问的。
如果我们以图表形式可视化可继承线程本地变量,它看起来会像这样。可以看到,InheritableThreadLocal映射是从父线程克隆而来的。
图片
五、注意事项
正如上图所清楚显示的,可继承线程本地变量所见到的优势也是一种缺点。默认情况下,当创建子线程时,可继承线程本地映射也会被克隆。但是你也可以看到,user指向父线程和子线程中相同的用户对象。
这意味着用户对象可以从多个线程访问,因此需要以线程安全的方式编写。换句话说,如果使用InheritableThreadLocal类,之前ThreadLocal类的线程安全性就会丢失。这对于你的设计可能是完全有效的。
然而,还有一种更安全的方法。我们可以在创建InheritableThreadLocal时指定一个childValue(..)方法。事实上,在下面的示例中,我们同时指定了一个初始值和一个子值。
public static final InheritableThreadLocal user
= new InheritableThreadLocal<>() {
@Override
protected User initialValue() {
return new User("anonymous");
}
@Override
protected User childValue(User parentValue) {
return new User(parentValue.getId());
}
};
在这种更改下,当Inheritable Thread Local映射被克隆时,与子线程关联的值将使用childValue(..)方法设置,该方法通过传递父线程的值来初始化每个Inheritable Thread Local。由于我们是从childValue(..)方法创建了一个新的对象,因此用户对象不会在父线程和子线程之间共享。通过这一改变,我们恢复了线程安全性,同时也能以只读的方式访问用户对象(通过有效地创建一个副本)。
同样,如果我们以图表形式可视化Inheritable Thread Locals,它看起来会像这样。很明显,现在用户分别指向父线程和子线程中的不同用户对象。
图片
希望这能让你对Java Thread Local变量有一个较好的理解,以及它们在应用程序中如何使用。