文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

深度解析Java Thread Locals工作原理

2024-11-29 22:39

关注

我们应该知道,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变量有一个较好的理解,以及它们在应用程序中如何使用。

来源:Java学研大本营内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯