【Java并发009】原理层面:ThreadLocal类全解析

2023-02-12,,,,

一、前言

在Java多线程模块中,ThreadLocal是比较重要的知识点,虽然ThreadLocal类位于java.lang包,但是这个类基本上仅用于多线程。

二、ThreadLocal类概要

2.1 由来 + 含义 + 使用场景

由来:一般的变量是多个线程共享,如果想一个线程独享一个变量,就需要用到ThreadLocal类

含义:ThreadLocal直译为线程局部变量,意思是ThreadLocal在每个线程中都创建了一个变量的副本(定义记住一句就好了),不同线程拥有的副本互不影响。

使用场景:

    在进行对象跨层传递的时候,可以避免多次传递,打破层次间的约束;
    线程间数据隔离;
    进行事务操作,用于存储线程事务信息;
    数据库连接,Session会话管理。

2.2 使用层面:ThreadLocal变量的线程隔离性

既然ThreadLocal的作用是每一个线程创建一个副本,我们使用一个例子来验证一下。创建两个线程,线程t1设置var值为20,线程t2设置var值为15,分别输出var值,运行结果如下:

tip:这里开发者调用具体构造函数、set()、get() 是ThreadLocal的主干方法,下面源码解析

public class Demo {
private static ThreadLocal<Integer> var = new ThreadLocal<>(); //构造函数
public static void main(String[] args) {
Thread t1 = new Thread(()->{
var.set(20); // 开发者调用set()方法
System.out.println(Thread.currentThread().getName() + ":设置var值为20");
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":获取var值为" + var.get()); // 开发者调用get()方法
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread1"); Thread t2 = new Thread(()->{
var.set(15); // 开发者调用set()方法
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":获取var值为" + var.get()); // // 开发者调用get()方法
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread2"); t1.start();
t2.start();
}
}

运行结果:

Thread1:设置var值为20
Thread1:获取var值为20
Thread2:获取var值为15
Thread2:获取var值为15
Thread1:获取var值为20
Thread2:获取var值为15
Thread1:获取var值为20

根据结果可以看出,ThreadLocal类变量在不同线程保存的变量副本是互不影响的,是相互隔离的。

三、ThreadLocal类源码解析

定位到ThreadLocal类,该类在java.lang包中。

3.1 主干方法,给开发者使用:set()方法

3.1.1 set()方法解读

public void set(T value) {
Thread t = Thread.currentThread(); // 当前线程
ThreadLocalMap map = getMap(t); // 获取Map
if (map != null) // map不为空,直接设置值
map.set(this, value);
else // map为空,创建map
createMap(t, value);
}

对于set()方法的解释:

    获取当前线程,并获取当前线程的ThreadLocalMap实例(从getMap(Thread t)中很容易看出来)。
    如果获取到的map实例不为空,调用map.set()方法,否则调用createMap(t, value)实例化map。

3.1.2 继续深入set(ThreadLocal<?> key, Object value)

我们来看下map.set(this, value)方法的具体实现:

private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算key的索引值
int i = key.threadLocalHashCode & (len-1);
// 根据获取到的索引进行循环,如果当前索引上的table[i]不为空,在没有return的情况下,就使用nextIndex()获取下一个(线性探测法)
// 下一个不为null,且没有return,继续下一个,知道e==null跳出循环
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get(); // 确定当前的k,下面两个if判断
// table[i]上key不为空,并且和方法参数key相同,就找到了,更新value,结束函数
if (k == key) {
e.value = value;
return;
}
// table[i]上的key为空,说明被回收了(弱引用)。
// 这个时候说明该table[i]可以重新使用,用新的key-value将其替换,并删除其他无效的entry
if (k == null) {
replaceStaleEntry(key, value, i); //分支方法里面详细解释
return;
}
}
// e==null,跳出循环,线性探测结束
// 找到为空的插入位置,插入值,在为空的位置插入需要对size进行加1操作
tab[i] = new Entry(key, value); // 方法参数中的key,value新建Entry,放到i
int sz = ++size;
// cleanSomeSlots用于清除那些e.get()==null,也就是table[index] != null && table[index].get()==null
// 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
// 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash()
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

在插入过程中,根据 ThreadLocal 对象的索引值,定位到 table 中的位置 i,过程如下:

    如果当前索引上的table[i]为空(金手指: e==null,跳出循环),那么正好,就初始化一个 Entry 对象放在位置 i 上;

    循环内的两个if判断:不巧,位置 i 已经有 Entry 对象了,如果这个 Entry 对象的 key 正好是即将设置的 key,则更新 Entry 中的 value值。如果Entry对象的key为null,则说明该table[i]可以重新使用,用新的key-value将其替换,并删除其他无效的entry;

    不断next循环:很不巧,位置 i 的 Entry 对象,和即将设置的 key 没关系,那么只能找下一个空位置;

3.1.3 继续深入,threadLocalHashCode变量

private static final int HASH_INCREMENT = 0x61c88647;
private final int threadLocalHashCode = nextHashCode(); private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode,每初始化一个 ThreadLocal对象,hash 值就增加一个固定的大小 0x61c88647。关于这个值和斐波那契散列有关,其原理这里不再深究,感兴趣可自行搜索,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中。

ThreadLocalMap使用线性探测法来解决哈希冲突,线性探测法的地址增量di = 1, 2, … , m-1,其中,i为探测次数。该方法一次探测一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

按照上面的描述,可以把table看成一个环形数组。

先看一下线性探测相关的代码,从中也可以看出来table实际是一个环:

private static int nextIndex(int i, int len) {
// 如果当前i不是最后一个,返回i+1,表示下一个,
// 值最后一个(就是i==len-1,最后一个),返回0,第一个,所以是环状
return ((i + 1 < len) ? i + 1 : 0);
}

可以发现,set()方法如果冲突严重的话,效率会很低。

3.2 主干方法,给开发者使用:get()

3.2.1 源码解析:get()

public T get() {
// 跟set方法类似,获取对应线程中的ThreadLocalMap实例
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 根据线程得到ThreadLocalMap实例 good
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 根据ThreadLocalMap实例得到ThreadLocalMap.Entry
if (e != null) { // 获得的Entry不为null,返回value(即 ThreadLocalMap.Entry)
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 为空返回初始化值
return setInitialValue();
}

对于get()方法解释:

    获取当前线程,并获取当前线程的ThreadLocalMap实例(从getMap(Thread t)中很容易看出来)。
    如果获取到的map实例不为空,调用map.getEntry(this)获取Entry对象,否则调用setInitialValue()实例化map。

3.2.2 继续深入getEntry()方法

private Entry getEntry(ThreadLocal<?> key) {
// 根据key计算索引,获取entry
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i]; // 根据索引i得到entry
if (e != null && e.get() == key) // 这里两个条件要同时满足
return e; //返回entry
else // 三种情况 e==null || e.get()!=key
// 因为用的是线性探测,所以往后找还是有可能能够找到目标Entry的
return getEntryAfterMiss(key, i, e); // 将方法参数中key传递过来,key得到的i传递过来,i对应的e传递过来,总结,就是key i entry
}

3.2.3 继续深入,getEntryAfterMiss()方法

// 通过计算出来的key找不到对应的value时使用这个方法,
// 结束getEntryAfterMiss()方法两种方式,不断后面找,直到找到或者都找不到e==null,返回null private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) { // 方法参数的e不为null,表示是通过e.get()!=key进来的
ThreadLocal<?> k = e.get(); // e.get() 得到对应的k
if (k == key)
return e; // 找到了,直接返回(第一个e不会走这一条路,后面 n-1 个才可能走这一条路)
if (k == null)
// 如果当前e中的k为null,清除无效的entry
expungeStaleEntry(i); // 传入索引,清除指定的entry
else
// 基于线性探测法向后扫描,找到下一个i,上面说了,是环状
i = nextIndex(i, len);
e = tab[i]; // 更新i后更新e
}
return null; //如果是因为e==null进来的,这个方法也无法处理,只能返回null
}

3.3 主干方法,给开发者使用:remove()

public void remove() {
// 根据线程得到ThreadLocalMap,这是可以理解的,因为一个线程对应一个ThreadLocal,而一个ThreadLocal对应一个ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this); // 然后根据ThreadLocalMap.remove(),将this传入
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算索引
int i = key.threadLocalHashCode & (len-1);
// 进行线性探测,查找正确的key
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) { // 找到了
// 调用weakrefrence的clear()清除引用
e.clear();
// 连续段清除
expungeStaleEntry(i);
return;
}
}
}
public void clear() {
this.referent = null; // 代码层面的清空就是设置为null,设置为null后,后面Java内存回收一定会处理的
}

remove()在有上面了解后可以说极为简单了,就是找到对应的table[],调用weakrefrence的clear()清除引用,然后再调用expungeStaleEntry()进行清除。

3.4 分支方法,辅助三个主干方法set() get() remove()

3.4.1 ThreadLocalMap 是何时初始化的(在3.1set() 3.2get()中使用)

第一,第一次调用set()方法时,如果ThreadLocalMap为null,则会调用createMap(t, value) 方法对ThreadLocalMap进行初始化

第一次调用set()方法时,如果ThreadLocalMap为null,则会调用createMap(t, value) 方法对ThreadLocalMap进行初始化,如:

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
} // 1、新建INITIAL_CAPACITY大小的Entry数组为table
// 2、在里面插入firstKey, firstValue
// 3、设置扩容阈值为初始容量的三分之二
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化一个大小 16 的 Entry 数组
// 表的大小始终为 2 的幂次
table = new Entry[INITIAL_CAPACITY];
// 计算 key 的 的 hash
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设定扩容阈值
setThreshold(INITIAL_CAPACITY);
} private void setThreshold(int len) {
threshold = len * 2 / 3; // 阈值为三分之二
}

关于& (INITIAL_CAPACITY - 1),这是取模的一种方式,对2的幂作为模数取模,用此代替%(2^n),这也就是为啥Entry的容量必须为2的幂。

第二,调用get()方法时,如果ThreadLocalMap为null,则会调用setInitialValue() 方法对ThreadLocalMap进行初始化,最终其实也是调用了createMap(t, value) 方法

调用get()方法时,如果ThreadLocalMap为null,则会调用setInitialValue() 方法对ThreadLocalMap进行初始化,最终其实也是调用了createMap(t, value) 方法,如:

private T setInitialValue() {
// 获取初始化值,默认为null(如果没有子类进行覆盖)
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 不为空不用再初始化,直接调用set操作设值
if (map != null)
map.set(this, value);
else
// 第一次初始化,createMap在上面有介绍过
createMap(t, value); // createMap(t,null); good
return value;
} protected T initialValue() {
return null;
}

3.4.2 replaceStaleEntry 替换无效Entry(唯一使用:在3.1set()方法中使用)

replaceStaleEntry 替换无效Entry(唯一使用:在3.1set()方法中使用) 

private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
// key和value是要设置的新值
// staleSlot 表示不新鲜的位置,该位置k==null,要替换swap 掉这个元素
Entry[] tab = table;
int len = tab.length;
Entry e;
/**
* 根据传入的无效entry的位置(staleSlot),向前扫描
* 一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
* 直到找到一个无效entry(k==null),或者扫描完也没找到
*/
int slotToExpunge = staleSlot; // 之后用于清理的起点,传递过来的参数确定 slotToExpunge 要删除的位置
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.get() == null) // 找一个无效entry e.get==null
slotToExpunge = i; //不断向前移动,更新slotToExpunge // 向后扫描一段连续的entry for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果找到了key,将其与传入的无效entry替换,也就是与table[staleSlot]进行替换 if (k == key) { //找到了
e.value = value; //因为找到了,所以设置value,参数中的value(要设置的value)赋值给tab[i].value,这里设置e.value,下面要 tab[staleSlot] = e; tab[i] = tab[staleSlot]; //无效的赋值给tab[i]
tab[staleSlot] = e; //当前的赋值给无效的 // 小结向前扫描结果
// slotToExpunge 如果向前查找没有找到无效entry,则更新slotToExpunge为当前值i
if (slotToExpunge == staleSlot) //向前没找到
slotToExpunge = i; // return之前要清空,所以调用cleanSomeSlots()方法
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return; // 这里是因为k==key 找到而结束
}
// 如果向前查找没有找到无效entry,并且当前向后扫描的entry无效,则更新slotToExpunge为当前值i
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i; // 向前扫描和向后扫描都没有成果
}
// 跳出了前进后退的循环,就是前面和后面都是e==null e=null
// 如果没有找到key(向后扫描找到key就return了),也就是说key之前不存在table中
// 就直接最开始的无效entry——tab[staleSlot]上直接新增即可 tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// slotToExpunge != staleSlot,说明存在其他的无效entry需要进行清理 good
// 小结向前扫描结果 slotToExpunge
//函数结束之前要清空,所以调用cleanSomeSlots()方法
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); //清理
}

3.4.3 cleanSomeSlots 启发式地清理slot(唯三调用:replaceStaleEntry()两次和set()一次)

/**
* 启发式的扫描清除,扫描次数由传入的参数n决定
*
* @param i 从i向后开始扫描(不包括i,因为索引为i的Slot肯定为null)
* @param n 控制扫描次数,正常情况下为 log2(n) ,
* 如果找到了无效entry,会将n重置为table的长度len,进行段清除。
* map.set()调用的时候传入的是元素个数,replaceStaleEntry()调用的时候传入的是table的长度len
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len); // 从i得到下一个i
Entry e = tab[i]; // 从i得到e Entry e = tab[nextIndex(i, len)];
if (e != null && e.get() == null) { // e不为null key为null 就知道了无效entry 注意,无效entry的判断依据是key==null
// 重置n为len
n = len;
removed = true;
// 依然调用expungeStaleEntry来进行无效entry的清除
i = expungeStaleEntry(i); // expungeStaleEntry方法返回一个让e==null对应的i
}
} while ( (n >>>= 1) != 0); // 无符号的右移动,可以用于控制扫描次数在log2(n)
return removed; //true 删除了 false 未删除
}

正常情况下如果 log n 次扫描没有发现无效 slot,函数就结束了,返回false。但是如果发现了无效的 slot,将 n 置为 table 的长度 len,做一次连续段的清理,再从下一个空的 slot 开始继续扫描。

金手指:如果发现了无效的 slot,将 n 置为 table 的长度 len,做一次连续段的清理,再从下一个空的 slot 开始继续扫描。

这个函数有两处地方会被调用,一处是map.set()会被调用,另外个是在替换无效slot时replaceStaleEntry()会被调用,区别是前者传入的 n 为元素个数,后者为 table 的容量。

3.4.4 expungeStaleEntry 连续段清除(多个方法中调用:cleanSomeSlots expungeStaleEntries getEntryAfterMiss remove replaceStaleEntry)

expungeStaleEntry
expunge 清除,staleEntry 无效entry

expungeStaleEntry 连续段清除,这个方法有什么用?

/**
* 连续段清除
* 1、根据传入的staleSlot,清理对应的无效entry——table[staleSlot],
* 2、并且根据当前传入的staleSlot,向后扫描一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
* 3、对可能存在hash冲突的entry进行rehash,并且清理遇到的无效entry.
*
* @param staleSlot key为null,需要无效entry所在的table中的索引
* @return 返回下一个为空的solt的索引。
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length; // 1、清理staleSlot指定的无效entry,置空,交给GC处理
tab[staleSlot].value = null;
tab[staleSlot] = null;
// 1、同时,size减1,置空后table的被使用量减1
size--; // Rehash until we encounter null // 不断rehash直到遇到null
Entry e;
int i;
/**
* 从staleSlot开始向后扫描一段连续的entry,这就是连续的含义
*/
for (i = nextIndex(staleSlot, len); // 第一次使用staleSlot得到下一个i
(e = tab[i]) != null; // e!=null 就不会跳出循环
i = nextIndex(i, len)) { // n-1次都是使用i得到下一个i
ThreadLocal<?> k = e.get(); // 第一个k
// 如果遇到key为null,表示无效entry,置空,交给GC处理
if (k == null) { //判断无效entry的条件是entry中k==null
e.value = null;
tab[i] = null;
size--;
} else {
// 如果key不为null,计算索引
int h = k.threadLocalHashCode & (len - 1);
/**
* 计算出来的索引h,与其现在所在位置的索引i不一致,置空当前的table[i]
* 从h开始向后线性探测到第一个空的slot,把当前的entry挪过去
*/
if (h != i) {
tab[i] = null; // while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
// 跳出循环是e==null,返回e对象的i,expungeStaleEntry方法返回一个让e==null对应的i
return i;
}

从 staleSlot 开始遍历,将无效 key(弱引用指向对象被回收)清理,即对应 entry 中的 value置为 null,将指向这个 entry 的 table[i]置为 null,直到扫到空 entry(e==null)。

另外,在过程中还会对非空的 entry 作 rehash。
可以说这个函数的作用就是从 staleSlot 开始清理连续段中的 slot(断开强引用,rehash slot等)

3.4.5 rehash(唯一调用set())

总步骤:先进行全量清理,如果清理后现有元素个数超过负载,那么进行扩容。

private void rehash() {
// 进行一次全量清理
expungeStaleEntries(); /**
* threshold = 2/3 * len
* 所以threshold - threshold / 4 = 1en/2
* 这里主要是因为上面做了一次全清理所以size减小,需要进行判断。
* 判断的时候把阈值调低了
*/
if (size >= threshold - threshold / 4) //如果size >= 1en/2
resize(); //扩容
}
全量清理expungeStaleEntries()

expunge是清理、stale是无效的、不新鲜的,entries是entry

/**
* 全清理,清理所有无效entry
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j]; // tab是Entry数组,所以每一个tab是一个entry对象
if (e != null && e.get() == null) // entry不为null key为null
// 使用连续段清理
expungeStaleEntry(j); // 清理指定下标j的entry
}
}

金手指:expungeStaleEntries()和expungeStaleEntry()是两个不同的方法
expungeStaleEntries()是一个循环调用,expungeStaleEntry()才是真正干事的

扩容(扩容建立新数组、打乱值、设置新阈值和新size,和hashmap的扩容如出一辙)

1、扩容:因为需要保证 table 的容量 len 为 2 的幂,所以扩容即扩大 2 倍
2、打乱值:oldTab 中的值,要计算下标h放到newTab中去 3、设置新阈值和新size:最后一步

/**
* 扩容,扩大为原来的2倍(这样保证了长度为2的幂)
*/
private void resize() {
Entry[] oldTab = table; // oldTable是有值的
int oldLen = oldTab.length;
int newLen = oldLen * 2; // 两倍
Entry[] newTab = new Entry[newLen]; // 但是现在newTab还是空的
int count = 0; for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j]; // 每一个oldTab中的值
if (e != null) {
ThreadLocal<?> k = e.get(); //entry中k就是ThreadLocal类型
// 虽然做过一次清理expungeStaleEntries()方法,但在扩容的时候可能会又存在key==null的情况
if (k == null) {
// 这里试试将e.value设置为null
e.value = null; // Help the GC 交给GC处理
} else {
// 同样使用线性探测来设置值
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e; // newTab[h] == null,设置
count++; // 每设置一个newTab,count++
}
}
}
// 设置新的阈值
setThreshold(newLen); // 传入newLen,设置新阈值为2/3 len
size = count; // count设置类变量size
table = newTab; // newTab有值了,放到类变量table里面
}

3.4.6 为什么 ThreadLocalMap 的 Key 是弱引用

ThreadLocalMap内部元素是Entry,Entry继承
static class Entry extends WeakReference<ThreadLocal<?>>
而且,Entry 的 key 永远都是 ThreadLocal 对象

如果是强引用,ThreadLocal 将无法被释放内存。

因为如果这里使用普通的 key-value 形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在 GC 分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。

弱引用是 Java 中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次 GC。当某个ThreadLocal 已经没有强引用可达,则随着它被垃圾回收,在 ThreadLocalMap 里对应的 Entry的键值会失效,这为 ThreadLocalMap 本身的垃圾清理提供了便利。

金手指:ThreadLocal内部类是ThreadLocalMap,ThreadLocalMap内部类是Entry

1、使用强引用,就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在 GC 分析中一直处于可达状态
2、使用弱引用,活不过下一次 GC。

3.4.7 ThreadLocalMap原理

public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
...
}

ThreadLocalMap是一个类似 HashMap 的数据结构,但是并没实现 Map 接口。内部初始化了一个大小为16 的 Entry 数组,Entry 对象用来保存每一个 key-value 键值对,只不过这里的 key 永远都是 ThreadLocal 对象(tip:Entry(ThreadLocal<?> k, Object v))。

当调用 ThreadLocal 对象的 set() 方法时,会把 ThreadLocal 对象自己当做 key(tip:因为这个Entry的key一定是ThreadLocal类型),当前线程对应的变量作为值(tip:即具体的值,如 20 或 15),存放进 ThreadLoalMap 中。

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 会把 ThreadLocal 对象自己当做 key
// 当前线程对应的变量(即具体的ThreadLocal变量,比如15或者20)作为值
map.set(this, value);
else
createMap(t, value);
}

tip:ThreadLoalMap 的 Entry 是继承 WeakReference,和 HashMap中的Entry很大的不同是,ThreadLoalMap 的Entry 中没有next 字段,所以就不存在链表的情况了。

3.4.8 内存泄漏问题

上图中我们可以看到,Entry对象的key是弱引用,当ThreadLocal被垃圾回收时,由于ThreadLocalMap是与线程绑定的,也即对应的Entry不会被回收,这时就会出现一个现象:ThreadLocalMap的key没了,但是value还存在,久而久之就会导致内存泄漏(tip:key是弱引用,但是value是具体指,ThreadLocalMap与线程绑定,Entry不会消失)。

既然存在内存泄露的隐患,自然有应对的策略,在上面的源码分析中,我们可以看到,在调用 ThreadLocal 的 get()、set()方法时都有清除 ThreadLocalMap 中 key 为 null 的 Entry 对象的操作。

 private void set(ThreadLocal<?> key, Object value) {
...
for (Entry e = tab[i];
...
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
...
}

这样对应的 value 就没有 GC Roots可达了,下次 GC 的时候就可以被回收。但get(),set()方法也有清理不完全的情况,所以使用完 ThreadLocal 之后,需要调用 remove() 方法进行清理。

JDK 建议将 ThreadLocal 变量定义成 private static 的,这样的话 ThreadLocal 的生命周期就更长,由于一直存在 ThreadLocal 的强引用,所以 ThreadLocal 也就不会被回收,也就能保证任何时候都能根据 ThreadLocal 的弱引用访问到 Entry 的 value 值。

小结:
优先级:
强引用>oom>软引用>弱引用
金手指:key是弱引用,但是value是具体指,ThreadLocalMap与线程绑定,Entry不会消失,所以,对于key==null,要将entry设置为null,这样一次GC既可以回收了

四、面试金手指

4.1 LocalThread类结构

LocalThread类结构

    一个线程一个ThreadLocalMap:每个Thread维护着一个ThreadLocalMap的引用,变量副本存储在线程自己的ThreadLocalMap中;

    ThreadLocalMap结构:ThreadLocalMap是ThreadLocal的内部类,用Entry来存储key-value,键值为ThreadLocal对象,value为线程变量;

    ThreadLocal仅仅作为key:ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

4.2 源码解析

就是上面的,略。

五、小结

ThreadLocal类全解析,完成了。

天天打码,天天进步!!!

【Java并发009】原理层面:ThreadLocal类全解析的相关教程结束。

《【Java并发009】原理层面:ThreadLocal类全解析.doc》

下载本文的Word格式文档,以方便收藏与打印。