ThreadLocal--介绍、常用场景

什么是ThreadLocal?

官方介绍

复制代码
/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 *
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // Atomic integer containing the next thread ID to be assigned
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // Thread local variable containing each thread's ID
 *     private static final ThreadLocal&lt;Integer&gt; threadId =
 *         new ThreadLocal&lt;Integer&gt;() {
 *             &#64;Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * <p>Each thread holds an implicit reference to its copy of a thread-local
 * variable as long as the thread is alive and the {@code ThreadLocal}
 * instance is accessible; after a thread goes away, all of its copies of
 * thread-local instances are subject to garbage collection (unless other
 * references to these copies exist).
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */
public class ThreadLocal<T> {
......
}
View Code
复制代码

  从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。

  我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

总结

1. 线程并发: 在多线程并发的场景下.

2. 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量.

3. 线程隔离: 每个线程的变量都是独立的,不会互相影响.

《Java并发编程之美》中介绍

  我们知道一个对象的所有线程共享它的全局变量,所以这些变量是非线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择ThreadLocal变量。每个线程都会拥有他们自己的Thread变量,它们可以使用get()/set()方法去获取他们的默认值或者在线程内部改变他们的值。

       ThreadLocal属于线程安全的实现方法—无同步方案的一种。ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而避免了线程安全问题。

示例

示例一

  开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。如果在打印之后调用本地变量的remove方法会删除本地内存中的变量。

复制代码
 1 public class ThreadLocalTest {
 2     static void print(String str) {
 3         //打印当前线程本地内存中localVariable变量的值
 4         System.out.println(str + ":" + localVariable.get());
 5         //清除当前线程本地内存中的localVariable的值
 6         localVariable.remove();
 7     }
 8 
 9     //创建ThreadLocal变量
10     static ThreadLocal<String> localVariable = new ThreadLocal<String>();
11 
12     //创建线程one
13     public static void main(String[] args) {
14         Thread threadOne = new Thread(new Runnable() {
15             @Override
16             public void run() {
17                 //设置线程One中本地变量loalVariable的值
18                 localVariable.set("threadOne local variable");
19                 //调用打印函数
20                 print("threadOne");
21                 //打印本地变量值
22                 System.out.println("threadOne remove after" + ":" + localVariable.get());
23             }
24         });
25         //创建线程two
26         Thread threadTwo = new Thread(new Runnable() {
27             @Override
28             public void run() {
29                 //设置线程Two中本地变量loalVariable的值
30                 localVariable.set("threadTwo local variable");
31                 //调用打印函数
32                 print("threadTwo");
33                 //打印本地变量值
34                 System.out.println("threadTwo remove after" + ":" + localVariable.get());
35             }
36         });
37         //启动线程
38         threadOne.start();
39         threadTwo.start();
40     }
41 
42 }
View Code
复制代码

示例二

我们来看下面这个案例 , 感受一下ThreadLocal 线程隔离的特点:

复制代码
 1 /**
 2  * 未使用ThreadLocal 
 3  * @author JustJavaIt
 4  * @date 2022/6/18 15:00
 5  */
 6 public class MyDemo {
 7     private String content;
 8 
 9     private String getContent() {
10         return content;
11     }
12 
13     private void setContent(String content) {
14         this.content = content;
15     }
16 
17     public static void main(String[] args) {
18         MyDemo demo = new MyDemo();
19         for (int i = 0; i < 5; i++) {
20             Thread thread = new Thread(new Runnable() {
21                 @Override
22                 public void run() {
23                     demo.setContent(Thread.currentThread().getName() + "的数据");
24                     System.out.println("-----------------------");
25                     System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
26                 }
27             });
28             thread.setName("线程" + i);
29             thread.start();
30         }
31     }
32 }
View Code
复制代码

从结果可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。

下面我们来看下采用 ThreadLocal 的方式来解决这个问题的例子。 

复制代码
 1 /**
 2  * 采用 ThreadLocal 的方式
 3  * @author justJavaIt
 4  * @date 2022/6/18 15:10
 5  */
 6 public class MyThreadLocalDemo {
 7     private static ThreadLocal<String> tl = new ThreadLocal<>();
 8 
 9     private String content;
10 
11     private String getContent() {
12         return tl.get();
13     }
14 
15     private void setContent(String content) {
16         tl.set(content);
17     }
18 
19     public static void main(String[] args) {
20         MyThreadLocalDemo demo = new MyThreadLocalDemo();
21         for (int i = 0; i < 5; i++) {
22             Thread thread = new Thread(new Runnable() {
23                 @Override
24                 public void run() {
25                     demo.setContent(Thread.currentThread().getName() + "的数据");
26                     System.out.println("-----------------------");
27                     System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
28                 }
29             });
30             thread.setName("线程" + i);
31             thread.start();
32         }
33     }
34 }
View Code
复制代码

从结果来看,这样很好的解决了多线程之间数据隔离的问题,十分方便。

 ThreadLocal两种典型的使用场景

场景1,ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。

场景2,ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。

典型场景1

这种场景通常用于保存线程不安全的工具类,典型的使用的类就是 SimpleDateFormat。

假如需求为1000 个线程都要用到 SimpleDateFormat,使用线程池来实现线程的复用,否则会消耗过多的内存等资源,如果我们每个任务都创建了一个 simpleDateFormat 对象,也就是说,1000 个任务对应 1000 个 simpleDateFormat 对象,那么运行结果正确,但是这么多对象的创建是有开销的,并且在使用完之后的销毁同样是有开销的,而且这么多对象同时存在在内存中也是一种内存的浪费。我们可以将simpleDateFormat 对象给提取了出来,变成 static 静态变量,但是这样一来就会有线程不安全的问题。我们希望达到的效果是,既不浪费过多的内存,同时又想保证线程安全。经过思考得出,可以让每个线程都拥有一个自己的 simpleDateFormat 对象来达到这个目的,这样就能两全其美了。要想达到这个目的,我们就可以使用 ThreadLocal。

ThreadLocal不支持继承

复制代码
 1 public class TestThreadLocal  {
 2 
 3     // (1) 创建线程变量
 4     public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 5     //测试InheritableThreadLocal让子线程可以访问在父程序中设置的本地变量。
 6     //public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
 7 
 8 
 9     public static void main(String[] args) {
10         // (2) 设置线程变量
11         threadLocal.set("hello world");
12         // (3) 启动子线程
13         Thread thread = new Thread(() -> {
14             // (4) 子线程输出线程变量的值
15             System.out.println("thread: " + threadLocal.get());
16         });
17 
18         thread.start();
19         // (5) 主线程获取并输出threadLocal的值
20         System.out.println("main: " + threadLocal.get());
21     }
22 }
View Code
复制代码

运行结果:

  同一个ThreadLocal变量在父线程中设置值后,在子线程是取不到的。这是正常现象。因为子线程thread里面调用get方法时当前线程为thread线程,而这里调用set方法设置的变量时main线程,两者是不同线程,自然子线程访问时放回null。

InheritableThreadLocal

  为了解决ThreadLocal不支持继承的问题,InheritableThreadLocal应运而生。InheritableThreadLocal继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。

示例只需把ThreadLocal不支持继承中示例的11行注释后打开13行即可。运行结果:

 可以看到现在可以从子线程正常获取到线程变量的值了。

原理

  InheritableThreadLocal 类通过重写getMap()和 createMap()方法 让本地变量保存到了具体线程的inheritableThreadLocals变量里面,那么线程再通过InheritableThreadLocal类实例的set或get方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面。

使用场景

  那么在什么情况下需要子线程可以获取父线程的threadLocal变量呢?还挺多比如,子线程需要拿到存放在threadLocal变量中的用户登录信息,有的中间件需要把统一的id追踪到的整个调用链路记录下来。其实子线程使用父线程中的threadLocal方法由多种方式,比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个map作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下InheritableThreadLocal就显得比较有用了。



感谢您的阅读,如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮。本文欢迎各位转载,但是转载文章之后必须在文章页面中给出作者和原文连接
posted @   JustJavaIt  阅读(660)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
历史上的今天:
2020-06-18 接口的不同写法在Swagger上的不同
点击右上角即可分享
微信分享提示