> ``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 这种存储结构的好处
1. 线程死去的时候,线程共享变量 ThreadLocalMap 则销毁。
1. ThreadLocalMap<ThreadLocal,Object> 键值对数量为 ThreadLocal 的数量,一般来说 ThreadLocal数量很少,相比在ThreadLocal中用 Map<Thread, Object> 键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。
## 3.3 ThreadLocalMap 大致实现
``ThreadLocalMap``是``ThreadLocal``的内部类,没有实现``Map``接口,用独立的方式实现了``Map``的功能(采用线性探测的方式解决Hash冲突,效率较低),其内部的``Entry``也独立实现``Entry``继承自``WeakReference``。
``Entry``中的 key 只能是``ThreadLocal``对象,这点已经被``Entry``的构造方法限定死了。
```java
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的成员变量:
```java
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,寻找下一个相邻的位置。
```java
/**
* 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<ThreadLocal, Object> 弱引用问题
``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.使用示例
```java
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`` 块进行回收。
---
可参考:
[ThreadLocal-面试必问深度解析](https://www.jianshu.com/p/98b68c97df9b)
[ThreadLocal内存泄漏真因探究](https://www.jianshu.com/p/a1cd61fa22da)
ThreadLocal