官网介绍
这个类提供线程局部变量。 这些变量与其正常的对应方式不同,因为访问一个的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)。
通俗点说
通俗点说就是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
在使用之前,我们先来认识几个ThreadLocal的常用方法:
| 方法声明 | 描述 |
|---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 将当前线程的此线程局部变量的副本设置为指定的值。 |
public T get() | 返回当前线程的此线程局部变量的副本中的值。 |
public void remove() | 删除此线程局部变量的当前线程的值。 |
protected T initialValue() | 返回此线程局部变量的当前线程的“初始值”。 |
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) | 创建线程局部变量。 |
class MyData {
ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(()->0);
public void add() {
threadLocalField.set(1 + threadLocalField.get());
}
}
public class ThreadLocalDemo01 {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(()->{
myData.add();
System.out.println(Thread.currentThread().getName() + "线程获取域值:" + myData.threadLocalField.get());
},String.valueOf(i)).start();
}
}
}

以上就实现了多线程情况下,每个线程独立域值且线程之间不影响。
以上的例子想必大家在思考这样通过synchronized不也可以实现嘛?确实,不过他们两的区别如下:
虽然ThradLocal模式 与 synchronized 关键字都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。
| synchronized | ThreadLocal | |
|---|---|---|
| 原理 | 同步机制采用 “以时间换空间” 的方式,只提供一份变量,让不同线程排队访问 | ThreadLocal采用“以空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不干扰 |
| 侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据互相隔离 |
总之,两者对应的业务场景是不一样的,虽然使用ThreadLocal和synchronized都能解决问题,但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。
根据阿里巴巴开发手册中表述:
必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。
class MyData {
ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(()->0);
public void add() {
threadLocalField.set(1 + threadLocalField.get());
}
}
当我们在线程池的情况下不进行清理自定义ThreadLocal变量时,其执行结果如何?
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q5U9ND4M-1656597743716)(JUC并发编程.assets/image-20220630174651436.png)]](https://1000bd.com/contentImg/2022/07/02/074810734.png)
我们发现在线程池的情况下线程是复用的,每次用完之后若不清空恢复到原始状态,就会影响后续业务逻辑。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jjrg8h7y-1656597743716)(JUC并发编程.assets/image-20220630175021417.png)]](https://1000bd.com/contentImg/2022/07/02/074811149.png)
常见的误解
想必大家许多人会和我最初的想法是一致的~
每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,从而达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早的 ThreadLocal 确实是这样设计的,但现在早已不是了。

现在的设计
但是,JDK后面优化了设计方案,在JDK8中 ThreadLocal 的设计是:
每个 Thread 维护一个 ThreadLocalMap,这个Map的 key 是 ThreadLocal 实例本身,value 才是真正要存储的值 object。
具体的过程是这样的:
这样设计方法的好处是:

首先我们通过 Thread、ThreadGroup、ThreadLocal 的关系来入手底层~


首先是 Thread类中维护了一个ThreadLocalMap,ThreadLocal类中包含了一个ThreadLocalMap静态内部类。
通过以上分析,我们了解到ThreadLocal的操作时机上是围绕着ThreadLocalMap展开的,ThreadLocalMap的源码相对比较复杂,我们从以下三个方面进行讨论~
ThreadLocalMap 是 ThreadLocal 的静态内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。类图大致如下:

1、 成员变量
/**
* 初始容量 -- 必须是2的整次幂。
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table,Entry类的定义在下面分析
* 同样,数组长度必须是2的整次幂
*/
private Entry[] table;
/**
* 数组里面entry的个数,可以用于判断table当前使用量是否超过阈值
*/
private int size = 0;
/**
* 进行扩容的阈值,表使用量大于它的时候进行扩容
*/
private int threshold; // Default to 0
跟 HashMap 类似,INITIAL_CAPACITY代表这个Map的初始容量;table 是一个Entry类型的数组,用于存储数据;size 代表表中的存储数目;threshold代表需要扩容时对应 size 的阈值。
2、存储结构-Entry
/**
* Entry 继承 WeakReference(弱引用),并且用ThreadLocal作为key。
* 如果key为null(entry.get() == null),意味着key不再被引用,因此这时候entry也可以从table中清除
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在ThreadLocalMap中,也是用Entry来保存k-v存储结构的,不过Entry中key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
我们在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,会有同学猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这里理解其实是不对的。
首先我们需要知道内存泄漏指的是什么呢?
Java中的引用有四种类型:强、软、弱、虚。
假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?

也就是说,如果ThreadLocalMap中的key使用了强引用,是无法完全避免内存泄漏的。
那么ThreadLocalMap中的key使用了弱引用,会出现内存泄漏吗?

也就是说,如果ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。
那出现内存泄漏的真实原因呢?
通过以上分析我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。那么内存泄漏的真正原因是什么呢?
细心的同学会发现,在以上两种内存泄漏的情况中,都有两个前提:
第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。
第二点稍微复杂一点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。
综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。
为什么使用弱引用
无论ThreadLocalMap中的key使用那种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强运用还是弱引用都不会有问题。那么为什么key要用弱引用呢?
我们知道避免内存泄漏有两种方式:
使用完ThreaadLocal,调用其remove方法删除对应的Entry
使用完ThreadLocal,当前Thread也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程是复用的。
也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null (即ThreadLocal为null) 进行判断,如果为null的话,那么是会对value置为null的。
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层屏障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任意方法的时候会被清除,从而避免内存泄漏。
接下来看看 initialValue、set、get、remove 四个方法的源码~
/**
* 设置此线程局部变量的当前线程副本
* @param value 要存储在该线程本地的当前线程副本中的值。
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取次线程对象中维护的 ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null) {
// 存在则调用map.set设置此实体entry
map.set(this, value);
} else {
// 当先线程Thread 不存在ThreadLocalMap对象,则调用createMap进行不存在ThreadLocalMap对象的初始化,并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
}
/**
* 获取与ThreadLocal
* @param 当前线程
* @return 当前线程对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

/**
* 创建当前线程Thread对应维护的ThreadLocalMap
* @param 当前线程
* @param 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
代码执行流程如下:
接下来我们来探究一下元素的存放过程:当调用set()方法时如果map为空,则会调用 createMap(Thread t, T firstValue)方法给线程创建 ThreadLocalMap ,并设制初始值。该方法调用了 ThreadLocalMap 的构造方法~
构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
/**
* firstKey:本ThreadLocal实例(this)
* firstValue:要保存的线程本地变量
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table,创建长度为16的 Entry数组
table = new Entry[INITIAL_CAPACITY];
// 计算索引(即在Entry数组中的存放位置)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 在指定索引的位置创建一个Entry存放键值对
table[i] = new Entry(firstKey, firstValue);
// 记录Map中键值对的个数,目前存放第一个entry,故为1
size = 1;
// 设置阈值
setThreshold(INITIAL_CAPACITY);
}
构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后将key,value封装成Entry存储在table数组中,并设置size和threshold。
重点分析:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 该语句解决了Hash冲突问题。
1、关于firstKey.threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// AtomicInteger 是一个提供原子操作的Intger类,通过线程安全的方式操作加减,适合高并发情况下的使用
private static AtomicInteger nextHashCode = new AtomicInteger();
// 特殊的hash值
private static final int HASH_INCREMENT = 0x61c88647;
这里定义了一个 AtomicInteger 类型(该方法有关介绍于本人CAS机制 博客),每次获取当前值并加上 HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是Entry[] table中,这样做可以尽量避免hash冲突。
2、关于 & (INITIAL_CAPACITY - 1)
计算hash的时候里面采用了 hashCode & (size-1) 的算法,这相当于取模运算 hashCode%size 的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。
ThreadLocalMap中的set方法
ok分析完map的构造方法,我们再来探究一下map的set方法。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算索引
int i = key.threadLocalHashCode & (len-1);
/**
* 使用先行探测
*/
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// ThreadLocal 对应的 key 存在,直接覆盖之前的值
if (k == key) {
e.value = value;
return;
}
// key为null,但是值不为null,说明之前 ThreadLocal 对象已经被回收了,当前数组下标的Entry是一个陈旧(stale)的元素
if (k == null) {
// 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry
tab[i] = new Entry(key, value);
int sz = ++size;
/**
* cleanSomeSlots 用于清除那些 e.get()==null的元素
* 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
* 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash(执行一次全表的扫描清除工作)
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
最后调用 cleanSomeSlots ,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz是否>=thresg达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。
再来谈谈 ThreadLocalMap 使用 线性探测法 来解决哈希冲突的。
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
比如说:假设当前table长度为16,也就是说如果计算出来的key的索引为14,如果table[14]上已经有值并且其key与当前key 不一致,那么就发生了hash冲突,这个时候将14+1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。可以把Entry[] table看成一个环形数组。
/**
* 返回此线程局部变量的当前线程副本中的值
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 对e进行判空
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值,并返回
T result = (T)e.value;
return result;
}
}
//1.map不存在,表示此线程没有维护的ThreadLocalMap对象,
//2.map存在,但是没有与当前ThreadLocal关联的entry
return setInitialValue();
}
/**
* 初始化
* @return 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值,此方法可以被子类重写,如果不重写默认返回null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 存在则调用map.set设置此实体entry
map.set(this, value);
} else {
// 当先线程Thread 不存在ThreadLocalMap对象,则调用createMap进行不存在ThreadLocalMap对象的初始化,并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
//返回设置的值value
return value;
}
代码执行流程如下:
总结:先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始者。
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null) {
// 存在则调用map.remove,删除以当前threadLocal为key对应的实体entry
m.remove(this);
}
}
代码执行流程如下:
/**
* 返回当前线程对应的ThreadLocal的初始值。
*/
protected T initialValue() {
return null;
}
此方法的第一次调用发生在:当线程通过get方法访问此线程的ThreadLocal值时。除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
通常情况下,每个线程最多调用一次这个方法。
这个方法仅仅简单的返回null,如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,必须通过子类继承 ThreadLocal 的方式去重写此方法,通常,可以通过匿名内部类的方式实现。如本篇博客中的demo~
此方法的作用是 返回该线程局部变量的初始值。
说了这么多 ThreadLocal,给大家小总结一下吧~