ThreadLocal
不是用来解决共享变量问题的,它与多线程的并发问题没有任何关系。
1.简介
早在 JDK 1.2 的版本中就提供Java.lang.ThreadLocal
,1.5 开始,ThreadLocal 开始支持泛型。ThreadLocal 为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
2.用法
ThreadLocal.get
: 获取ThreadLocal中当前线程共享变量的值。ThreadLocal.set
: 设置ThreadLocal中当前线程共享变量的值。ThreadLocal.remove
: 移除ThreadLocal中当前线程共享变量的值。ThreadLocal.initialValue
: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。
3.原理
3.1 线程共享变量缓存
Thread.ThreadLocalMap<ThreadLocal, Object>
Thread
: 当前线程,可以通过Thread.currentThread()
获取。ThreadLocal
:我们的static ThreadLocal
变量。Object
: 当前线程共享变量。
我们调用 ThreadLocal.get 方法时,实际上是从当前线程中获取 ThreadLocalMap<ThreadLocal, Object>
,然后根据当前 ThreadLocal 获取当前线程共享变量 Object。
ThreadLocal.set,ThreadLocal.remove实际上是同样的道理。
3.2 这种存储结构的好处
- 线程死去的时候,线程共享变量 ThreadLocalMap 则销毁。
- ThreadLocalMap
键值对数量为 ThreadLocal 的数量,一般来说 ThreadLocal数量很少,相比在ThreadLocal中用 Map 键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。
3.3 ThreadLocalMap 大致实现
ThreadLocalMap
是ThreadLocal
的内部类,没有实现Map
接口,用独立的方式实现了Map
的功能(采用线性探测的方式解决Hash冲突,效率较低),其内部的Entry
也独立实现Entry
继承自WeakReference
。
Entry
中的 key 只能是ThreadLocal
对象,这点已经被Entry
的构造方法限定死了。
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
Entry
继承自WeakReference
(弱引用,生命周期只能存活到下次 GC 前),但只有Key
是弱引用类型的(注意看 3.1 中的虚线),Value
并非弱引用。
ThreadLocalMap的成员变量:
static class ThreadLocalMap {
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}
3.4 ThreadLocalMap 的 Hash 冲突怎么解决
和HashMap
的最大的不同在于,ThreadLocalMap
结构非常简单,没有 next 引用,也就是说ThreadLocalMap
中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经有其他 key 值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap
解决 Hash 冲突的方式就是简单的步长加 1 或减 1,寻找下一个相邻的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
所以建议:每个线程只存一个
ThreadLocal
变量,这样的话所有的线程存放到 map 中的 Key 都是相同的ThreadLocal
,如果一个线程要保存多个变量,就需要创建多个ThreadLocal
,多个ThreadLocal
放入 Map 中时会极大的增加Hash冲突的可能。
3.5 ThreadLocalMap 弱引用问题
Entry
继承自WeakReference
(3.3 讲述过),这就导致了一个问题:ThreadLocal
在没有外部对象强引用时,发生 GC 时弱引用 Key 会被回收,而 Value 不会回收,如果创建ThreadLocal
的线程一直持续运行,但是 ThreadLocal 已经被回收,那么这个 Entry 对象中的 value 就有可能一直得不到回收(线程中存在ThreadLocalMap<null, Object>
的键值对),发生内存泄露。(ThreadLocal
被回收,ThreadLocal
关联的线程共享变量还存在)。
JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()
方法调用的时候会清除掉线程ThreadLocalMap中所有 Entry 中 Key 为 null 的 Value,并将整个 Entry 设置为 null,利于下次内存回收。
但这样也并不能保证ThreadLocal不会发生内存泄漏,例如:
- 使用 static 的 ThreadLocal,延长了 ThreadLocal 的生命周期,可能导致的内存泄漏。
- 虽然
ThreadLocal
的get,set
方法可以清除ThreadLocalMap
中key为null的value,但是get,set
方法在内存泄露后并不会必然调用。
所以为了防止此类情况的出现,我们有两种手段:
- 1.使用完线程共享变量后,显式调用
ThreadLocalMap.remove
方法清除线程共享变量; - 2.JDK建议
ThreadLocal
定义为private static
,这样ThreadLocal
的弱引用问题则不存在了(我并不确定)。
3.6 为什么使用弱引用?
从表面上看,发生内存泄漏,是因为 Key 使用了弱引用类型。但其实是因为整个 Entry 的 key 为 null 后,没有主动清除 value 导致。很多文章大多分析 ThreadLocal 使用了弱引用会导致内存泄漏,但为什么使用弱引用而不是强引用?
官方说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。
分析一下:
- 假如key 使用强引用:引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。
- 假如key 使用弱引用:引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap调用
set,get,remove
的时候会被清除。
比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal
不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 的 value 就会导致内存泄漏,而不是因为弱引用。
在使用线程池的情况下,没有及时清理 ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。
4.使用示例
public class Run {
private static ThreadLocal tl = new ThreadLocal();
public static void main(String[] args) {
if (tl.get() == null) {
System.out.println("从未放过值");
tl.set("我的值");
}
System.out.println(tl.get());
System.out.println(tl.get());
}
}
5.Java开发手册(阿里巴巴)对ThreadLocal的要求
【参考】 ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题。
说明:这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。
必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally
块进行回收。
评论区