前言
ThreadLocal
很多同学都搞不懂是什么东西,可以用来干嘛。但面试时却又经常问到,所以这次我和大家一起学习ThreadLocal
这个类。
下面我就以面试问答的形式学习我们的——ThreadLocal
类(源码分析基于JDK8)
本文同步发布于简书 :www.jianshu.com/p/807686414…
问答内容
1.
问:ThreadLocal
了解吗?您能给我说说他的主要用途吗?
答:
- 从JAVA官方对
ThreadLocal
类的说明定义(定义在示例代码中):ThreadLocal
类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get
和set
方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal
实例通常来说都是private static
类型的,用于关联线程和线程上下文。 - 我们可以得知
ThreadLocal
的作用是:ThreadLocal
的作用是提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量的传递的复杂度。 - 上述可以概述为:
ThreadLocal
提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。
示例代码:
1 | 复制代码 |
- 其中
nextHashCode()
方法就是一个原子类不停地去加上0x61c88647,这是一个很特别的数,叫斐波那契散列(Fibonacci Hashing),斐波那契又有一个名称叫黄金分割,也就是说将这个数作为哈希值的增量将会使哈希表的分布更为均匀。
2.
问:ThreadLocal
实现原理是什么,它是怎么样做到局部变量不同的线程之间不会相互干扰的?
答:
- 通常,如果我不去看源代码的话,我猜
ThreadLocal
是这样子设计的:每个ThreadLocal
类都创建一个Map
,然后用线程的IDthreadID
作为Map
的key
,要存储的局部变量作为Map
的value
,这样就能达到各个线程的值隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal
就是这样设计的。 - 但是,JDK后面优化了设计方案,现时JDK8
ThreadLocal
的设计是:每个Thread
维护一个ThreadLocalMap
哈希表,这个哈希表的key
是ThreadLocal
实例本身,value
才是真正要存储的值Object
。 - 这个设计与我们一开始说的设计刚好相反,这样设计有如下几点优势:
1) 这样设计之后每个Map
存储的Entry
数量就会变小,因为之前的存储数量由Thread
的数量决定,现在是由ThreadLocal
的数量决定。
2) 当Thread
销毁之后,对应的ThreadLocalMap
也会随之销毁,能减少内存的使用。
ThreadLocal引用关系图- 图片来自于《简书 - 对ThreadLocal实现原理的一点思考》
上述解释主要参考自:ThreadLocal和synchronized的区别?
3.
问:您能说说ThreadLocal
常用操作的底层实现原理吗?如存储set(T value)
,获取get()
,删除remove()
等操作。
答:
- 调用
get()
操作获取ThreadLocal
中对应当前线程存储的值时,进行了如下操作:
1 ) 获取当前线程Thread
对象,进而获取此线程对象中维护的ThreadLocalMap
对象。
2 ) 判断当前的ThreadLocalMap
是否存在:
- 如果存在,则以当前的
ThreadLocal
为key
,调用ThreadLocalMap
中的getEntry
方法获取对应的存储实体 e。找到对应的存储实体 e,获取存储实体 e 对应的value
值,即为我们想要的当前线程对应此ThreadLocal
的值,返回结果值。 - 如果不存在,则证明此线程没有维护的
ThreadLocalMap
对象,调用setInitialValue
方法进行初始化。返回setInitialValue
初始化的值。 setInitialValue
方法的操作如下:
1 ) 调用initialValue
获取初始化的值。
2 ) 获取当前线程Thread
对象,进而获取此线程对象中维护的ThreadLocalMap
对象。
3 ) 判断当前的ThreadLocalMap
是否存在:
- 如果存在,则调用
map.set
设置此实体entry
。 - 如果不存在,则调用
createMap
进行ThreadLocalMap
对象的初始化,并将此实体entry
作为第一个值存放至ThreadLocalMap
中。
PS:关于ThreadLocalMap
对应的相关操作,放在下一个问题详细说明。
示例代码:
1 | 复制代码 /** |
- 调用
set(T value)
操作设置ThreadLocal中对应当前线程要存储的值时,进行了如下操作:
1 ) 获取当前线程Thread
对象,进而获取此线程对象中维护的ThreadLocalMap
对象。
2 ) 判断当前的ThreadLocalMap
是否存在:
- 如果存在,则调用
map.set
设置此实体entry
。 - 如果不存在,则调用
createMap
进行ThreadLocalMap
对象的初始化,并将此实体entry
作为第一个值存放至ThreadLocalMap
中。
示例代码:
1 | 复制代码 /** |
- 调用
remove()
操作删除ThreadLocal中对应当前线程已存储的值时,进行了如下操作:
1 ) 获取当前线程Thread
对象,进而获取此线程对象中维护的ThreadLocalMap
对象。
2 ) 判断当前的ThreadLocalMap
是否存在, 如果存在,则调用map.remove
,以当前ThreadLocal
为key
删除对应的实体entry
。
- 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18复制代码 /**
* 删除当前线程中保存的ThreadLocal对应的实体entry
* 如果此ThreadLocal变量在当前线程中调用 {@linkplain #get read}方法
* 则会通过调用{@link #initialValue}进行再次初始化,
* 除非此值value是通过当前线程内置调用 {@linkplain #set set}设置的
* 这可能会导致在当前线程中多次调用{@code initialValue}方法
*
* @since 1.5
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
4.
问:对ThreadLocal
的常用操作实际是对线程Thread
中的ThreadLocalMap
进行操作,核心是ThreadLocalMap
这个哈希表,你能谈谈ThreadLocalMap
的内部底层实现吗?
答:
ThreadLocalMap
的底层实现是一个定制的自定义HashMap
哈希表,核心组成元素有:
1 ) Entry[] table;
:底层哈希表 table, 必要时需要进行扩容,底层哈希表 table.length 长度必须是2的n次方。
2 ) int size;
:实际存储键值对元素个数 entries
3 ) int threshold;
:下一次扩容时的阈值,阈值 threshold = 底层哈希表table的长度 len * 2 / 3
。当size >= threshold
时,遍历table
并删除key
为null
的元素,如果删除后size >= threshold*3/4
时,需要对table
进行扩容(详情请查看set(ThreadLocal<?> key, Object value)
方法说明)。
- 其中
Entry[] table;
哈希表存储的核心元素是Entry
,Entry
包含:
1 ) ThreadLocal<?> k;
:当前存储的ThreadLocal
实例对象
2 ) Object value;
:当前 ThreadLocal 对应储存的值value
- 需要注意的是,此
Entry
继承了弱引用WeakReference
,所以在使用ThreadLocalMap
时,发现key == null
,则意味着此key ThreadLocal
不在被引用,需要将其从ThreadLocalMap
哈希表中移除。(弱引用相关问题解释请查看 问答 5)
示例代码:
1 | 复制代码 /** |
ThreadLocalMap
的构造方法是延迟加载的,也就是说,只有当线程需要存储对应的ThreadLocal
的值时,才初始化创建一次(仅初始化一次)。初始化步骤如下:
1) 初始化底层数组table
的初始容量为 16。
2) 获取ThreadLocal
中的threadLocalHashCode
,通过threadLocalHashCode & (INITIAL_CAPACITY - 1)
,即ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的长度 length 的方式计算该实体的存储位置。
3) 存储当前的实体,key 为 : 当前ThreadLocal value:真正要存储的值
4)设置当前实际存储元素个数 size 为 1
5)设置阈值setThreshold(INITIAL_CAPACITY)
,为初始化容量 16 的 2/3。
示例代码:
1 | 复制代码 |
ThreadLocal
的get()
操作实际是调用ThreadLocalMap
的getEntry(ThreadLocal<?> key)
方法,此方法快速适用于获取某一存在key
的实体entry
,否则,应该调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法获取,这样做是为了最大限制地提高直接命中的性能,该方法进行了如下操作:
1 ) 计算要获取的entry
的存储位置,存储位置计算等价于:ThreadLocal
的 hash
值 threadLocalHashCode
% 哈希表的长度 length
。
2 ) 根据计算的存储位置,获取到对应的实体 Entry
。判断对应实体Entry
是否存在 并且 key
是否相等:
- 存在对应实体
Entry
并且对应key
相等,即同一ThreadLocal
,返回对应的实体Entry
。 - 不存在对应实体
Entry
或者key
不相等,则通过调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法继续查找。 getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
方法操作如下:
1 ) 获取底层哈希表数组table
,循环遍历对应要查找的实体Entry
所关联的位置。
2 ) 获取当前遍历的entry
的 key ThreadLocal
,比较key
是否一致,一致则返回。
3 ) 如果key
不一致 并且 key
为 null
,则证明引用已经不存在,这是因为Entry
继承的是WeakReference
,这是弱引用带来的坑。调用expungeStaleEntry(int staleSlot)
方法删除过期的实体Entry
(此方法不单独解释,请查看示例代码,有详细注释说明)。
4 ) key
不一致 ,key
也不为空,则遍历下一个位置,继续查找。
5 ) 遍历完毕,仍然找不到则返回null
。
示例代码:
1 | 复制代码 |
ThreadLocal
的set(T value)
操作实际是调用ThreadLocalMap
的set(ThreadLocal<?> key, Object value)
方法,该方法进行了如下操作:
1 ) 获取对应的底层哈希表table
,计算对应threalocal
的存储位置。
2 ) 循环遍历table
对应该位置的实体,查找对应的threadLocal
。
3 ) 获取当前位置的threadLocal
,如果key threadLocal
一致,则证明找到对应的threadLocal
,将新值赋值给找到的当前实体Entry
的value
中,结束。
4 ) 如果当前位置的key threadLocal
不一致,并且key threadLocal
为null
,则调用replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)
方法(此方法不单独解释,请查看示例代码,有详细注释说明),替换该位置key == null
的实体为当前要设置的实体,结束。
5 ) 如果当前位置的key threadLocal
不一致,并且key threadLocal
不为null
,则创建新的实体,并存放至当前位置 i tab[i] = new Entry(key, value);
,实际存储键值对元素个数size + 1
,由于弱引用带来了这个问题,所以要调用cleanSomeSlots(int i, int n)
方法清除无用数据(此方法不单独解释,请查看示例代码,有详细注释说明),才能判断现在的size
有没有达到阀值threshhold
,如果没有要清除的数据,存储元素个数仍然 大于 阈值 则调用rehash
方法进行扩容(此方法不单独解释,请查看示例代码,有详细注释说明)。
示例代码:
1 | 复制代码 |
ThreadLocal
的remove()
操作实际是调用ThreadLocalMap
的remove(ThreadLocal<?> key)
方法,该方法进行了如下操作:
1 ) 获取对应的底层哈希表 table
,计算对应threalocal
的存储位置。
2 ) 循环遍历table
对应该位置的实体,查找对应的threadLocal
。
3 ) 获取当前位置的threadLocal
,如果key threadLocal
一致,则证明找到对应的threadLocal
,执行删除操作,删除此位置的实体,结束。
示例代码:
1 | 复制代码 |
5.
问:ThreadLocalMap
中的存储实体Entry
使用ThreadLocal
作为key
,但这个Entry
是继承弱引用WeakReference
的,为什么要这样设计,使用了弱引用WeakReference
会造成内存泄露问题吗?
答:
- 首先,回答这个问题之前,我需要解释一下什么是强引用,什么是弱引用。
我们在正常情况下,普遍使用的是强引用:
1 | 复制代码A a = new A(); |
当 a = null;b = null;
时,一段时间后,JAVA垃圾回收机制GC会将 a 和 b 对应所分配的内存空间给回收。
但考虑这样一种情况:
1 | 复制代码C c = new C(b); |
当 b 被设置成null
时,那么是否意味这一段时间后GC工作可以回收 b 所分配的内存空间呢?答案是否定的,因为即使 b 被设置成null
,但 c 仍然持有对 b 的引用,而且还是强引用,所以GC不会回收 b 原先所分配的空间,既不能回收,又不能使用,这就造成了 内存泄露。
那么如何处理呢?
可以通过c = null;
,也可以使用弱引用WeakReference w = new WeakReference(b);
。因为使用了弱引用WeakReference
,GC是可以回收 b 原先所分配的空间的。
上述解释主要参考自:对ThreadLocal实现原理的一点思考
- 回到
ThreadLocal
的层面上,ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,如果一个ThreadLocal
没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal
势必会被回收,这样一来,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
其实,ThreadLocalMap
的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal
的get()
,set()
,remove()
的时候都会清除线程ThreadLocalMap
里所有key
为null
的value
。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用
static
的ThreadLocal
,延长了ThreadLocal
的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。 - 分配使用了
ThreadLocal
又不再调用get()
,set()
,remove()
方法,那么就会导致内存泄漏。
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal
使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
我们先来看看官方文档的说法:
1 | 复制代码To help deal with very large and long-lived usages, |
为了应对非常大和长时间的用途,哈希表使用弱引用的 key
。
下面我们分两种情况讨论:
key
使用强引用:引用的ThreadLocal
的对象被回收了,但是ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致Entry
内存泄漏。key
使用弱引用:引用的ThreadLocal
的对象被回收了,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。value
在下一次ThreadLocalMap
调用get()
,set()
,remove()
的时候会被清除。- 比较两种情况,我们可以发现:由于
ThreadLocalMap
的生命周期跟Thread
一样长,如果都没有手动删除对应key
,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal
不会内存泄漏,对应的value
在下一次ThreadLocalMap
调用get()
,set()
,remove()
的时候会被清除。
因此,ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key
就会导致内存泄漏,而不是因为弱引用。
综合上面的分析,我们可以理解ThreadLocal
内存泄漏的前因后果,那么怎么避免内存泄漏呢?
每次使用完ThreadLocal
,都调用它的remove()
方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal
,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal
就跟加锁完要解锁一样,用完就清理。
上述解释主要参考自:深入分析 ThreadLocal 内存泄漏问题
6.
问:ThreadLocal
和synchronized
的区别?
答:ThreadLocal
和synchronized
关键字都用于处理多线程并发访问变量的问题,只是二者处理问题的角度和思路不同。
ThreadLocal
是一个Java类,通过对当前线程中的局部变量的操作来解决不同线程的变量访问的冲突问题。所以,ThreadLocal
提供了线程安全的共享对象机制,每个线程都拥有其副本。- Java中的
synchronized
是一个保留字,它依靠JVM的锁机制来实现临界区的函数或者变量的访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作“锁机制”的变量时多个线程共享的。
- 同步机制(
synchronized
关键字)采用了以“时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal
采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。
7.
问:ThreadLocal
在现时有什么应用场景?
答:总的来说ThreadLocal
主要是解决2种类型的问题:
- 解决并发问题:使用
ThreadLocal
代替synchronized
来保证线程安全。同步机制采用了“以时间换空间”的方式,而ThreadLoca
l采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。 - 解决数据存储问题:
ThreadLocal
为变量在每个线程中都创建了一个副本,所以每个线程可以访问自己内部的副本变量,不同线程之间不会互相干扰。如一个Parameter
对象的数据需要在多个模块中使用,如果采用参数传递的方式,显然会增加模块之间的耦合性。此时我们可以使用ThreadLocal
解决。
应用场景:
Spring
使用ThreadLocal
解决线程安全问题
- 我们知道在一般情况下,只有无状态的
Bean
才可以在多线程环境下共享,在Spring
中,绝大部分Bean
都可以声明为singleton
作用域。就是因为Spring
对一些Bean
(如RequestContextHolder
、TransactionSynchronizationManager
、LocaleContextHolder
等)中非线程安全状态采用ThreadLocal
进行处理,让它们也成为线程安全的状态,因为有状态的Bean
就可以在多线程中共享了。 - 一般的
Web
应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程ThreadLocal
是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal
比直接使用synchronized
同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
示例代码:
1 | 复制代码public abstract class RequestContextHolder { |
总结
ThreadLocal
提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。ThreadLocal
的设计是:每个Thread
维护一个ThreadLocalMap
哈希表,这个哈希表的key
是ThreadLocal
实例本身,value
才是真正要存储的值Object
。- 对
ThreadLocal
的常用操作实际是对线程Thread
中的ThreadLocalMap
进行操作。 ThreadLocalMap
的底层实现是一个定制的自定义HashMap
哈希表,ThreadLocalMap
的阈值threshold
= 底层哈希表table
的长度len * 2 / 3
,当实际存储元素个数size
大于或等于 阈值threshold
的3/4
时size >= threshold*3/4
,则对底层哈希表数组table
进行扩容操作。ThreadLocalMap
中的哈希表Entry[] table
存储的核心元素是Entry
,存储的key
是ThreadLocal
实例对象,value
是ThreadLocal
对应储存的值value
。需要注意的是,此Entry
继承了弱引用WeakReference
,所以在使用ThreadLocalMap
时,发现key == null
,则意味着此key ThreadLocal
不在被引用,需要将其从ThreadLocalMap
哈希表中移除。ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,如果一个ThreadLocal
没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal
势必会被回收。所以,在ThreadLocal
的get()
,set()
,remove()
的时候都会清除线程ThreadLocalMap
里所有key
为null
的value
。如果我们不主动调用上述操作,则会导致内存泄露。- 为了安全地使用
ThreadLocal
,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal
后都要调用remove()
来清理无用的Entry
。这在操作在使用线程池时尤为重要。 ThreadLocal
和synchronized
的区别:同步机制(synchronized
关键字)采用了以“时间换空间”的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal
采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。ThreadLocal
主要是解决2种类型的问题:A. 解决并发问题:使用ThreadLocal
代替同步机制解决并发问题。B. 解决数据存储问题:如一个Parameter
对象的数据需要在多个模块中使用,如果采用参数传递的方式,显然会增加模块之间的耦合性。此时我们可以使用ThreadLocal
解决。
参考文章
深入浅出ThreadLocal
ThreadLocal和synchronized的区别?
深入剖析ThreadLocal
ThreadLocal内部机制
聊一聊Spring中的线程安全性
对ThreadLocal实现原理的一点思考
深入分析 ThreadLocal 内存泄漏问题
学习Spring必学的Java基础知识(6)—-ThreadLocal
ThreadLocal设计模式
ThreadLocal案例分析
Spring单例模式与线程安全ThreadLocal
本文转载自: 掘金