Java并发之ThreadLocal
ThreadLocal是什么?有哪些用途?
ThreadLocal是什么?
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
以上来源官方API。大概可以总结为两点:
- ThreadLocal提供get/set方法,可以访问属于当前线程的变量,也就是可以保证每个线程的变量不一样。
- ThreadLocal使用时通常定义为private static的。
从字面意思理解,可能会将ThreadLocal认为是本地线程,其实ThreadLocal并不是线程,而是线程Thread的局部变量。
ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说无法获取到数据。
使用场景
- ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,每个线程都只能修改自己所拥有的副本, 而不会影响其他线程的副本,这样就让原本在并发情况下,线程不安全的情况变成了线程安全的情况。
- ThreadLocal 用作每个线程内需要独立保存信息的场景,供其他方法更方便得获取该信息,每个线程获取到的信息都可能是不一样的,前面执行的方法设置了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参。
使用方式
ThreadLocal 类接口很简单,只有 4 个方法,我们先来了解一下:
- void set(Object value)
设置当前线程的线程局部变量的值。 - public Object get()
该方法返回当前线程所对应的线程局部变量。 - public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。 - protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第 1 次调用 get() 或 set(Object)时才执行,并且仅执行 1 次。ThreadLocal 中的缺省实现直接返回一 个 null。
使用起来出奇的简单,仅仅使用set()
和get()
方法即可:
public class Main { |
打印结果:
线程一的数据 --- threadLocalOne |
源码分析
那么ThreadLocal是如何做到保证每个线程get出来的数据不一样的呢?
为什么通过 ThreadLocal 可以在不同的线程中维护一套数据的副本并且彼此互不干扰?
我们通过源码来看一下。
线程隔离
在上面的ThreadLocal的使用中,我们发现一个很有趣的事情,ThreadLocal在不同的线程,好像能够存储不同的数据:就好像ThreadLocal本身具有存储功能,到了不同线程,能够生成不同的'副本'存储数据一样
实际上,ThreadLocal到底是怎么做到的呢?
来看下set()方法,看看到底怎么存数据的:此处涉及到ThreadLocalMap类型,暂且把他当成Map,详细的后面栏目分析
其实这地方做了一个很有意思的操作:线程数据隔离的操作,是Thread类和ThreadLocal类相互配合做到的
在下面的代码中可以看出来,在塞数据的时候,会获取执行该操作的当前线程
拿到当前线程,取到threadLocals变量,然后仿佛以当前实例为key,数据value的形式往这个map里面塞值(有区别,set栏目再详细说)
所以使用ThreadLocal在不同的线程中进行写操作,实际上数据都是绑定在当前线程的实例上,ThreadLocal只负责读写操作,并不负责保存数据,这就解释了,为什么ThreadLocal的set数据,只在操作的线程中有用
大家有没有感觉这种思路有些巧妙!
//数据 |
Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系
先要搞清楚 Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系。我们用最直观、最容易理解的图画的方式来看看它们三者的关系:
我们看到最左下角的 Thread 1,这是一个线程,它的箭头指向了 ThreadLocalMap 1,其要表达的意思是,每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量,在这里 Thread 1 所拥有的成员变量就是 ThreadLocalMap 1。
而这个 ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对。那么我们就来看一下它的 key 和 value 分别是什么。可以看到这个表格的左侧是 ThreadLocal 1、ThreadLocal 2…… ThreadLocal n,能看出这里的 key 就是 ThreadLocal 的引用。
而在表格的右侧是一个一个的 value,这就是我们希望 ThreadLocal 存储的内容,例如 user 对象等。
这里需要重点看到它们的数量对应关系:一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。
通过这张图片,我们就可以搞清楚 Thread、 ThreadLocal 及 ThreadLocalMap 三者在宏观上的关系了。
get 方法
首先我们来看一下 get 方法,源码如下所示:
public T get() { |
这是 ThreadLocal 的 get 方法,可以看出它利用了 Thread.currentThread 来获取当前线程的引用,并且把这个引用传入到了 getMap 方法里面,来拿到当前线程的 ThreadLocalMap。
然后就是一个 if ( map != null ) 条件语句,那我们先来看看 if (map == null) 的情况,如果 map == null,则说明之前这个线程中没有创建过 ThreadLocalMap,于是就去调用 setInitialValue 来创建;如果 map != null,我们就应该通过 this 这个引用(也就是当前的 ThreadLocal 对象的引用)来获取它所对应的 Entry,同时再通过这个 Entry 拿到里面的 value,最终作为结果返回。
值得注意的是,这里的 ThreadLocalMap 是保存在线程 Thread 类中的,而不是保存在 ThreadLocal 中的。
分析:
- 一个线程对应一个 ThreadLocalMap,get() 就是当前线程获取自己的 ThreadLocalMap。
- 线程根据使用那一小块的 threadlocal,根据 ThreadLocal 对象作为 key,去获取存储于 ThreadLocalMap 中的值。
getMap 方法
下面我们来看一下 getMap 方法,源码如下所示:
ThreadLocalMap getMap(Thread t) { |
可以看到,这个方法很清楚地表明了 Thread 和 ThreadLocalMap 的关系,可以看出 ThreadLocalMap 是线程的一个成员变量。这个方法的作用就是获取到当前线程内的 ThreadLocalMap 对象,每个线程都有 ThreadLocalMap 对象,而这个对象的名字就叫作 threadLocals,初始值为 null,代码如下:
ThreadLocal.ThreadLocalMap threadLocals = null; |
set 方法
下面我们再来看一下 set 方法,源码如下所示:
public void set(T value) { |
set 方法的作用是把我们想要存储的 value 给保存进去。可以看出,首先,它还是需要获取到当前线程的引用,并且利用这个引用来获取到 ThreadLocalMap ;然后,如果 map == null 则去创建这个 map,而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去。
可以看出,map.set(this, value) 传入的这两个参数中,第一个参数是 this,就是当前 ThreadLocal 的引用,这也再次体现了,在 ThreadLocalMap 中,它的 key 的类型是 ThreadLocal;而第二个参数就是我们所传入的 value,这样一来就可以把这个键值对保存到 ThreadLocalMap 中去了。
我们发现在set方法中,会创建一个ThreadLocalMap,然后将要设置的值放在这个Map中,而当前这个ThreadLocal对象作为key;
然后再将这个ThreadLocalMap赋值给Thread的threadLocals里。如果去看Thread类的代码会发现,在Thread类中存在两个变量threadLocals和inheritableThreadLocals,它们的类型就是ThreadLocal.ThreadLocalMap。通过下图可以看出Thread,ThreadLocal,ThreadLocalMap三者的关系。
分析:
- 一个线程对应一个 ThreadLocalMap ,可以存储多个 ThreadLocal 对象。
- ThreadLocal 对象作为key、独享数据作为value。
- ThreadLocalMap 可参考 HashMap,在 ThreadMap 里面存在 Entry 数组也就是一个 Entry 一个键值对。
ThreadLocalMap 类,也就是 Thread.threadLocals
下面我们来看一下 ThreadLocalMap 这个类,下面这段代码截取自定义在 ThreadLocal 类中的 ThreadLocalMap 类:
static class ThreadLocalMap { |
ThreadLocalMap 类是每个线程 Thread 类里面的一个成员变量,其中最重要的就是截取出的这段代码中的 Entry 内部类。在 ThreadLocalMap 中会有一个 Entry 类型的数组,名字叫 table。我们可以把 Entry 理解为一个 map,其键值对为:
- 键,当前的 ThreadLocal;
- 值,实际需要存储的变量,比如 user 用户对象或者 simpleDateFormat 对象等。
ThreadLocalMap 既然类似于 Map,所以就和 HashMap 一样,也会有包括 set、get、rehash、resize 等一系列标准操作。但是,虽然思路和 HashMap 是类似的,但是具体实现会有一些不同。
比如其中一个不同点就是,我们知道 HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链,如下图所示:
但是 ThreadLocalMap 解决 hash 冲突的方式是不一样的,它采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。这是 ThreadLocalMap 和 HashMap 在处理冲突时不一样的点。
使用场景
通过上面的内容基本可以掌握ThreadLocal的基本用法,那么ThreadLocal主要在什么场景中使用呢。
ThreadLocal的作用通过以上了解我们知道主要是用来做线程间数据隔离。那么在什么场景下能用到线程隔离呢?
首先想到的就是SimpleDateFormat这个工具类,它不是线程安全的,可以通过ThreadLocal在每个线程中放一份,保证线程安全。
还有比如说用户登录的session,或者token数据,只数据当前会话线程,也可以通过ThreadLocal存储。
再比如在某些场景下,上下文数据在不同方法之间调用,传递起来非常麻烦,可以通过ThreadLocal存放,只需要在需要用到的地方获取就可以。
除了这些场景,在某些框架源码中也会使用到,比如Spring中的事务也主要是通过ThreadLocal和面向切面编程AOP实现的,感兴趣的同学可以查看源码了解。
内存泄漏
什么是内存泄漏
内存泄漏指的是,当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏。
因为通常情况下,如果一个对象不再有用,那么我们的垃圾回收器 GC,就应该把这部分内存给清理掉。这样的话,就可以让这部分内存后续重新分配到其他的地方去使用;否则,如果对象没有用,但一直不能被回收,这样的垃圾对象如果积累的越来越多,则会导致我们可用的内存越来越少,最后发生内存不够用的 OOM 错误。
下面我们来分析一下,在 ThreadLocal 中这样的内存泄漏是如何发生的。
Key 的泄漏
在上面我们分析了 ThreadLocal 的内部结构,知道了每一个 Thread 都有一个 ThreadLocal.ThreadLocalMap 这样的类型变量,该变量的名字叫作 threadLocals。线程在访问了 ThreadLocal 之后,都会在它的 ThreadLocalMap 里面的 Entry 中去维护该 ThreadLocal 变量与具体实例的映射。
我们可能会在业务代码中执行了 ThreadLocal instance = null 操作,想清理掉这个 ThreadLocal 实例,但是假设我们在 ThreadLocalMap 的 Entry 中强引用了 ThreadLocal 实例,那么,虽然在业务代码中把 ThreadLocal 实例置为了 null,但是在 Thread 类中依然有这个引用链的存在。
GC 在垃圾回收的时候会进行可达性分析,它会发现这个 ThreadLocal 对象依然是可达的,所以对于这个 ThreadLocal 对象不会进行垃圾回收,这样的话就造成了内存泄漏的情况。
JDK 开发者考虑到了这一点,所以 ThreadLocalMap 中的 Entry 继承了 WeakReference 弱引用,代码如下所示:
static class Entry extends WeakReference<ThreadLocal<?>> { |
可以看到,这个 Entry 是 extends WeakReference。弱引用的特点是,如果这个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,所以弱引用不会阻止 GC。因此,这个弱引用的机制就避免了 ThreadLocal 的内存泄露问题。
这就是为什么 Entry 的 key 要使用弱引用的原因。
- Key 使用强引用:也就是上述说的情况,引用 ThreadLocal 的对象被回收了,ThreadLocal 的引用 ThreadLocalMap 的 Key 为强引用并没有被回收,如果不手动回收的话,ThreadLocal 将不会回收那么将导致内存泄漏。
- Key 使用弱引用:引用的 ThreadLocal 的对象被回收了,「ThreadLocal的引用 ThreadLocalMap 的 Key 为弱引用,如果内存回收,那么将ThreadLocalMap 的 Key 将会被回收,ThreadLocal 也将被回收。value 在ThreadLocalMap 调用 get、set、remove 的时候就会被清除」。
- 比较两种情况,我们可以发现:由于
ThreadLocalMap
的生命周期跟Thread
一样长,如果都没有手动删除对应key
,都会导致内存泄漏,但是使用弱引用可以多一层保障:「弱引用ThreadLocal
不会内存泄漏,对应的value
在下一次ThreadLocalMap
调用set
,get
,remove
的时候会被清除」。
Value 的泄漏
可是,如果我们继续研究的话会发现,虽然 ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,但是这个 Entry 包含了一个对 value 的强引用,还是刚才那段代码:
static class Entry extends WeakReference<ThreadLocal<?>> { |
可以看到,value = v 这行代码就代表了强引用的发生。
正常情况下,当线程终止,key 所对应的 value 是可以被正常垃圾回收的,因为没有任何强引用存在了。但是有时线程的生命周期是很长的,如果线程迟迟不会终止,那么可能 ThreadLocal 以及它所对应的 value 早就不再有用了。在这种情况下,我们应该保证它们都能够被正常的回收。
为了更好地分析这个问题,我们用下面这张图来看一下具体的引用链路(实线代表强引用,虚线代表弱引用):
可以看到,左侧是引用栈,栈里面有一个 ThreadLocal 的引用和一个线程的引用,右侧是我们的堆,在堆中是对象的实例。
我们重点看一下下面这条链路:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例
。
这条链路是随着线程的存在而一直存在的,如果线程执行耗时任务而不停止,那么当垃圾回收进行可达性分析的时候,这个 Value 就是可达的,所以不会被回收。但是与此同时可能我们已经完成了业务逻辑处理,不再需要这个 Value 了,此时也就发生了内存泄漏问题。
JDK 同样也考虑到了这个问题,在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。
但是假设 ThreadLocal 已经不被使用了,那么实际上 set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value 的内存泄漏。
如何避免内存泄露
分析完这个问题之后,该如何解决呢?解决方法就是我们本课时的标题:调用 ThreadLocal 的 remove 方法。调用这个方法就可以删除对应的 value 对象,可以避免内存泄漏。
我们来看一下 remove 方法的源码:
public void remove() { |
可以看出,它是先获取到 ThreadLocalMap 这个引用的,并且调用了它的 remove 方法。这里的 remove 方法可以把 key 所对应的 value 给清理掉,这样一来,value 就可以被 GC 回收了。
所以,在使用完了 ThreadLocal 之后,我们应该手动去调用它的 remove 方法,目的是防止内存泄漏的发生。
总结
ThreadLocalMap
内部Entry
中key
使用的是对ThreadLocal
对象的弱引用,这为避免内存泄露是一个进步,因为如果是强引用,那么即使其他地方没有对ThreadLocal
对象的引用,ThreadLocalMap
中的ThreadLocal
对象还是不会被回收,而如果是弱引用则这时候ThreadLocal
引用是会被回收掉的。- 但是对于的
value
还是不能被回收,这时候ThreadLocalMap
里面就会存在key
为null
但是value
不为null
的entry
项,虽然ThreadLocalMap
提供了set
,get
,remove
方法在一些时机下会对这些 Entry 项进行清理,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露,所以在使用完毕后即使调用remove
方法才是解决内存泄露的最好办法。 - 线程池里面设置了
ThreadLocal
变量一定要记得及时清理,因为线程池里面的核心线程是一直存在的,如果不清理,那么线程池的核心线程的threadLocals
变量一直会持有ThreadLocal
变量。
参考感谢
https://mp.weixin.qq.com/s/MpImTSSTG5qeuXMKATYdkQ
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=285
https://mp.weixin.qq.com/s/7XEIqTIv8W8CNNYnWqe2rw