深度解析Java Thread Locals工作原理

2024年 5月 29日 109.5k 0

一、前言

在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变量指向的是不同的用户对象。

深度解析Java Thread Locals工作原理-1图片

三、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映射是从父线程克隆而来的。

深度解析Java Thread Locals工作原理-2图片

五、注意事项

正如上图所清楚显示的,可继承线程本地变量所见到的优势也是一种缺点。默认情况下,当创建子线程时,可继承线程本地映射也会被克隆。但是你也可以看到,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 Locals工作原理-3图片

希望这能让你对Java Thread Local变量有一个较好的理解,以及它们在应用程序中如何使用。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论