ThreadLocal详解
本篇文章会带你从源码的角度讲解给你ThreadLocal的一些特点以及为什么可以实现线程间数据隔离,坐好位置,我们上车啦~
什么是ThreadLocal?
ThreadLocal是java.lang下面的一个类,是用来解决java多线程程序中并发问题的一种途径;通过为每一个线程创建一份共享变量的副本来保证各个线程之间的变量的访问和修改互相不影响;
ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这样处理后,能够优雅的解决一些实际问题。比如一次用户的页面操作请求,我们可以在最开始的filter中,把用户的信息保存在ThreadLocal中,在同一次请求中,再使用到用户信息,就可以直接到ThreadLocal中获取就可以了。
ThreadLocal的结构
ThreadLocal中用于保存线程的独有变量的数据结构是一个内部类:ThreadLocalMap,也是k-v结构。key就是当前的ThreadLocal对象,而v就是我们想要保存的值。Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组。
这个图上还有一些需要说的的点我们在后面的知识点中进行讲解~
ThreadLocal的一些特点
-
线程隔离性:
- 每个线程都拥有自己独立的
ThreadLocal
变量副本。 - 一个线程对
ThreadLocal
变量的修改不会影响其他线程中的副本数据,线程之间的ThreadLocal
数据是完全隔离的。
- 每个线程都拥有自己独立的
从源码角度看:
在访问ThreadLocal的时候,实际上是先获取到当前线程的ThreadLocalMap实例,然后再通过当前 ThreadLocal 作为 Key 去匹配键值对。
-
全局唯一,线程局部:
ThreadLocal
是全局唯一的,即在一个 Java 程序中,ThreadLocal
是通过一个对象来引用的。但是它的值对于不同的线程是局部的,每个线程都有自己独立的副本。
-
不能跨线程共享数据:
ThreadLocal
的设计目的是为了保证每个线程访问的数据是独立的,因此它不能在不同线程之间共享数据。- 如果一个线程通过
ThreadLocal
修改了数据,其他线程无法感知这个变化。
-
懒初始化:
ThreadLocal
变量的初始化是懒加载的。通过ThreadLocal.withInitial()
或ThreadLocal.get()
方法获取值时,ThreadLocal
会自动为每个线程分配一个初始值(如果没有提前设置)。- 如果线程第一次访问
ThreadLocal
变量,它会使用withInitial()
中的初始化函数来为该线程生成初始值。
-
可以避免线程安全问题:
- 由于每个线程都有自己的独立副本,多个线程对
ThreadLocal
变量进行读写时无需同步,因此避免了传统的并发问题,如竞争条件(race condition)。
- 由于每个线程都有自己的独立副本,多个线程对
-
自动清理:
- 当线程结束时(比如线程池中的线程回收时),该线程的
ThreadLocal
变量会被垃圾回收。这避免了内存泄漏的问题。 - 但是,
ThreadLocal
变量本身不会在应用程序层面自动清理。如果没有手动清除(remove()
),可能会导致内存泄漏,尤其是在使用线程池时。
- 当线程结束时(比如线程池中的线程回收时),该线程的
-
适合使用场景:
ThreadLocal
适用于存储线程局部数据,如数据库连接、用户会话等。- 它可以避免不同线程间的锁竞争,提升性能。常见的应用场景包括线程池中每个线程的缓存数据、日志跟踪等。
-
性能优势:
- 在使用
ThreadLocal
时,因为每个线程都持有独立的副本,所以无需加锁来同步对数据的访问。这相较于使用共享资源时的锁机制,ThreadLocal
可以提高性能,尤其是在高并发环境下。
- 在使用
ThreadLocal为什么可以实现线程间数据隔离?
其实,在上面我们说ThreaLocal的特点的时候,我们就已经提到了为什么可以实现线程间数据隔离。就拿set方法源码举例,他首先是获取到当前线程的ThreadLocalMap实例,然后再通过当前ThreadLocal匹配对应Entry,就相当于在初始化的时候,每一个线程都存储了同一个数据的副本数据,因此在修改的时候也只会在获取到当前线程的ThreadLocalMap实例后,通过当前ThreadLocal去找到当前线程存储的对应的副本数据,这样才实现线程间数据隔离。
ThreadLocal内存泄漏问题
在我们讲ThreadLocal的结构的时候,我们就应该思考一个问题:
对于ThreadLocal的引用,一个是在栈上,一个是在堆上,如果栈上的ThreadLocal Ref引用不再使用了,即方法结束后这个对象引用就不再用了,那么,ThreadLocal对象因为还有一条引用链在,所以就会导致他无法被回收,久而久之可能就会对导致OOM。
另外一种情况即ThreadLocal在多线程当中,线程一直被使用,就会导致这条引用链一直在在使用,也会导致ThreadLocal无法被回收。
当然JDK帮我们解决了一部分问题(主要是解决上面说的第一个问题),先看看下面的源码:
这段代码就是Entry的构造方法代码,ThreadLocal的k传递给了Entry类的父类的WeakReference方法,就相当于ThreadLocalMap中键是ThreadLocal弱引用。
当然我们还看到除了ThreadLocal外还有一条引用,那就是value的引用,这条引用并不是弱引用,那怎么解决呢?
在我们调用ThreadLocal的get、set、remove方法的时候,实际上调用的是ThreadLocalMap的get、set、remove方法,而ThreadLocalMap的每次get、set、remove,都会清理key为null,但是value还存在的Entry。
所以当我们每次调用完ThreadLocal后,手动调用remove方法,保证在下一次GC的时候清理掉Entry。
ThreadLocal的应用场景
ThreadLocal的应用场景还有其他的场景使用,以下列举两条参考:
用户身份信息存储: 在很多应用中,都需要做登录鉴权,一旦鉴权通过之后,就可以把用户信息存储在ThreadLocal中,这样在后续的所有流程中,需要获取用户信息的,直接取ThreadLocal中获取就行了。非常的方便。
traceId存储:和上面存储日志上下文类似,在分布式链路追踪中,需要存储本次请求的traceId,通常也都是基于ThreadLocal存储的。
以上就是对ThreadLocal的一个整体介绍啦,如果你还有其他补充请在评论区留言,我们共同学习~