Java版本:8u261。
ThreadLocal是线程本地变量(缓存),其往往用来实现在同一线程内部的变量之间进行交互的情景,不存在线程之间的交互。其对每一个线程内部都维护了一个数据,在a线程set的值,也只能在a线程里进行get。
具体的使用场景:比如可以用ThreadLocal来封装数据库连接;也可以在复杂逻辑下,用ThreadLocal来作为方法之间的数据传递:如果一开始设置了一个数据,但因为调用逻辑复杂,跨越了很多的类和方法后,需要再次获取到这个数据,那么这个时候就可以用ThreadLocal来对这个数据进行缓存,访问就很方便了。但需要注意的是,这一切操作都必须保证是在同一个线程中进行的,也就是在同一个线程中进行set,也在同一个线程中进行get。在Spring框架中除了使用HashMap和ConcurrentHashMap来做各种缓存之外,也会用到ThreadLocal来缓存数据。
初学ThreadLocal可能会认为它跟synchronized和ReentrantLock是同样的同步机制(我之前也写过对ReentrantLock进行源码分析的文章《较真儿学源码系列-AQS(逐行源码带你分析作者思路)》),但实际上它们完全是两种东西。实现的方式不同,解决的场景也不同。synchronized和ReentrantLock使用的是时间换空间的思路,是用来在多线程场景中访问共享变量用的。获取不到资源的线程会进行排队,等待去获取;而ThreadLocal使用空间换时间的做法,是用来做数据隔离和单个线程内的数据共享用的。set方法只会在当前线程中缓存数据,而get方法也只会在当前线程中获取到这个数据,在别的线程中调用get方法是获取不到的(在别的线程中调用get方法只会获取到ThreadLocal在那个线程中缓存的值)。
ThreadLocal内部维护了一个静态内部类ThreadLocalMap,ThreadLocal内部的所有操作都是在其中实现的(另外提一嘴:我看过很多讲ThreadLocal源码分析的文章,但他们都没有分析ThreadLocalMap的实现。没有分析到ThreadLocalMap这个层面,当然会觉得ThreadLocal的实现很简单。因为ThreadLocal只能说是个包装,核心的操作都在ThreadLocalMap里面):
static class ThreadLocalMap {
static class Entry extends WeakReference> {
/**
* The value associated with this ThreadLocal.
*/
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
//...
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
//...
}
可以看到其中也维护了一个Entry数组table,而Entry是ThreadLocalMap中的静态内部类,并且Entry类本质上是一个包装ThreadLocal的弱引用,其内部维护着一个强引用的value属性,存放的便是ThreadLocal中需要存储的值。
而ThreadLocalMap是放到了Thread类的内部属性中,由ThreadLocal来进行维护:
public class Thread implements Runnable {
//...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
//...
}
也就是说每个线程中都会有一份缓存副本,每个线程可以访问自己内部的副本,而该副本对于其他线程来说是隔离的。总结来说,在同一个线程中的多个ThreadLocal,会通过hash算法放入到当前线程中的threadLocals属性中的table数组中,也就是哈希槽。只不过不同于HashMap遇到hash冲突就采用挂链表的方式,ThreadLocal采用的是线性探测(开放地址)的方式来放入的。
ThreadLocal内部会维护着一条从Thread的引用->Thread->ThreadLocalMap->Entry->Entry的值的引用链,如下图所示:
其上的虚线为弱引用,实线为强引用。当使用完Thread对象后需要被回收时,在下一次gc的时候,因为Entry连着的ThreadLocal引用是弱引用,所以Thread对象能够顺利被回收掉。而如果是强引用,并且ThreadLocal是用static修饰的话,可能就不会被回收,从而产生内存泄漏的问题。
虽然上面使用了弱引用,以此来保证Thread对象能够成功被回收掉。但是正如上面ThreadLocalMap的源码所示,Entry其中的value仍为强引用。所以如果使用不当的话,仍然会造成内存泄露的问题出现。考虑这么一种情况:如果使用完ThreadLocal、同时其引用断开,并且没有其他的强引用,则在下一次gc的时候,这个ThreadLocal对象就会被回收,变为null。但是因为Entry中的value是强引用,所以此时在threadLocals的Entry数组中就会有一个ThreadLocal为null,但是其中的value仍然有值的Entry。此时就再也不能通过原来的ThreadLocal(此时为null)来访问到该value了。而该线程却一直存在(比如说是线程池),Thread又强引用着ThreadLocalMap,因此ThreadLocalMap也不会被回收。于是就产生了Entry中value的内存泄露。
解决办法是再一次调用get/set/remove方法,这三个方法在其实现的内部逻辑中都会遍历删除Entry为null的值,以此来避免内存泄漏的发生(get和set方法只能删除部分垃圾数据,而且可能在还没有遍历到ThreadLocal为null的Entry时,这两个方法就已经提前成功返回了。所以最好是使用remove方法。在使用完ThreadLocal后显式调用一次remove方法,这将会把当前这个ThreadLocal对应的Entry删除掉,从根本上杜绝了内存泄漏的发生。在后面的源码分析中可以看到这点)。
进一步思考:如果将value也置为弱引用行不行?如果这么做的话,value除了Entry这个弱引用之外,就再没有别的引用了。这样的话在下一次gc时value值就会被清除掉,而Thread对象却一直存活者,再次调用就会返回null。这是绝对不能容忍的,因为这个时候就不是内存泄不泄漏的问题了,此时就变成了程序的bug(丢失数据)。
这是《阿里巴巴编码规范》中的一条。SimpleDateFormat因为其内部的Calendar属性而存在线程安全问题,如果把其定义成static的成员变量,多个线程同时更改获取它,就可能会出现问题。解决方法是使用Java 8中新添加的时间API,或者使用第三方的时间类库(Joda-Time)。当然使用ThreadLocal来包装SimpleDateFormat的方式也不失为一种好的解决办法:
private static final ThreadLocal SDF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String str) throws ParseException {
return SDF.get().parse(str);
}
public static String format(Date date) {
return SDF.get().format(date);
}
/**
* ThreadLocal:
*/
public ThreadLocal() {
//空实现
}
虽然ThreadLocal的构造器是空实现,但是同时会完成hashCode的计算,如下所示:
public class ThreadLocal {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
/*
nextHashCode是AtomicInteger类型的,这里是在获取当前nextHashCode的值,然后会加上
HASH_INCREMENT。注意这里的nextHashCode是static修饰的,也就是类变量。也就是说,
每调用一次ThreadLocal的构造器,就会生成一个不一样的hashCode
*/
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//...
}
不同于HashMap中计算hashCode是采用key的hashCode方法高低16位异或的方式,在ThreadLocal中计算hashCode是使用了一个固定的值0x61c88647,那么为什么会是这个数呢?其实0x61c88647换算成十进制就是1640531527。而Java中的int是32位,也就是2654435769 = -1640531527。这里的,其中的φ也就是所谓的黄金分割率,也就是近似于0.618的那个数。而黄金比例又与斐波那契数()之间有密切关系,使用这个数时会使得散列的结果很均匀:
/**
* ThreadLocal:
*/
public void set(T value) {
//获取当前的线程
Thread t = Thread.currentThread();
//获取线程中的threadLocals
ThreadLocalMap map = getMap(t);
if (map != null)
//如果threadLocals存在,就往其中存放数据(this代表当前的ThreadLocal)
map.set(this, value);
else
//否则就完成ThreadLocalMap的初始化并放入数据
createMap(t, value);
}
/**
* 第8行代码处:
*/
ThreadLocalMap getMap(Thread t) {
//返回上面说过的Thread中的threadLocals,也就是当前线程的缓存副本
return t.threadLocals;
}
/**
* 第14行代码处:
* 首先来看一下初始化的过程
*/
void createMap(Thread t, T firstValue) {
/*
前面看到在ThreadLocal的构造器中是空实现,而在set方法中的此处完成ThreadLocalMap的延迟初始化,
初始化完成后赋值给当前线程的threadLocals属性
*/
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* ThreadLocalMap构造器
*/
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
//对Entry数组table进行初始化,初始容量INITIAL_CAPACITY为16
table = new Entry[INITIAL_CAPACITY];
//获取哈希槽的位置。这里的按位与也就是在做threadLocalHashCode对数组容量取余的结果
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//对哈希槽做初始化(只初始化当前槽的位置,也体现了延迟初始化的思想)
table[i] = new Entry(firstKey, firstValue);
//计数为1
size = 1;
//设置threshold
setThreshold(INITIAL_CAPACITY);
}
/**
* 第46行代码处:
*/
Entry(ThreadLocal> k, Object v) {
//这里会将当前ThreadLocal放入到弱引用中
super(k);
//这里会将要存入ThreadLocal的值存入到Entry的value中
value = v;
}
/**
* 第50行代码处:
* 设置threshold值(threshold相当于HashMap中负载因子的概念),可以看到这里的策略是数组容量的2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 第11行代码处:
* 再来看一下set方法中、如果当前线程中的ThreadLocalMap已经初始化了,就会执行本方法完成set操作
*/
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//和上面ThreadLocalMap构造器中的一样,这里是在获取哈希槽的位置
int i = key.threadLocalHashCode & (len - 1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//获取当前槽中的Entry中保存的ThreadLocal
ThreadLocal> k = e.get();
/*
如果这个ThreadLocal就是当前线程的ThreadLocal的话,就更新一下value值,也就是做值的覆盖。然后返回
如果不等,就说明发生了hash冲突,此时继续寻找下一个哈希槽,也就是用线性探测的方式(nextIndex(i, len))
*/
if (k == key) {
e.value = value;
return;
}
/*
如果当前槽不为null,但是其中保存的ThreadLocal为null,说明这个弱引用被回收了(上面说过Entry继承弱引用)
此时会删除这个位置的垃圾数据(以及其他无效的位置),防止内存泄漏
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
/*
走到这里说明上面的for循环全走完了也没有找到key相等的节点或key是null的节点,此时的i就是下一个要插入的槽位
那么现在就把新的value数据插入到这个位置中就行了
*/
tab[i] = new Entry(key, value);
//计数+1
int sz = ++size;
/*
当存放完数据后,此时会查看是否需要清理垃圾数据,如果没有垃圾数据的话,
并且当前数组的容量大于等于threshold阈值,就进行“扩容”
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 第102行代码处:
*/
private void replaceStaleEntry(ThreadLocal> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
/*
从staleSlot位置起往前寻找第一个ThreadLocal为null的哈希槽位置(也就是垃圾数据)
这里没有直接用staleSlot而是用了一次遍历确定下来的slotToExpunge是因为:这里不光会
删除staleSlot位置的垃圾数据,还会把所有的垃圾数据都删除
*/
int slotToExpunge = staleSlot;
//向前环形搜索垃圾数据
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//向后环形搜索
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//获取当前槽中的Entry中保存的ThreadLocal
ThreadLocal> k = e.get();
//如果key(ThreadLocal)相同的话
if (k == key) {
//就做一下值的覆盖
e.value = value;
/*
同时会将staleSlot(垃圾数据位置处)和i位置的数据做一下交换
(因为tab[i]的数据已经在上面缓存了,所以不需要暂存变量)
交换之后,垃圾数据就到了i处(这样就可以保证垃圾数据最后会放在
相同key的最后一个哈希槽位置处,保证了hash的顺序)
*/
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
/*
如果在之前的向前环形搜索的过程中没有找到垃圾数据的话,就把i赋值给slotToExpunge,
意思是以当前i位置处作为清理的起点
*/
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//找到垃圾数据并进行清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
/*
如果在之前的向前环形搜索的过程中没有找到垃圾数据,但是在此时的向后环形搜索的过程中找到了,
就一样把i赋值为slotToExpunge,以当前i位置处作为清理的起点
*/
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//如果在上面的循环中没有找到相同的key的话,此时将垃圾数据位置处的value置为null,去掉强引用
tab[staleSlot].value = null;
//同时将一个新的Entry赋值进去(也就是当前需要set进去的数据)
tab[staleSlot] = new Entry(key, value);
//如果在上面向后环形搜索的过程中找到了垃圾数据的话,就一样需要进行清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 第138行和第140行代码处:
* 根据指定的哈希槽位置查找上一个位置处,如果已经到第一个位置了,就重新返回最后一个位置处
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* 第84行、第145行、第147行、第226行、第228行、第253行、第275行和第353行代码处:
* 根据指定的哈希槽位置查找下一个位置处,如果已经到最后一个位置了,就重新返回第一个位置处
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 第172行、第191行、第285行和第320行代码处:
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//删除垃圾数据位置处的强引用value和Entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
//计数-1
size--;
Entry e;
int i;
//从staleSlot位置处向后环形搜索
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//获取当前槽中的Entry中保存的ThreadLocal
ThreadLocal> k = e.get();
if (k == null) {
//如果再次遇到垃圾数据,就将其清理掉,并且计数-1
e.value = null;
tab[i] = null;
size--;
} else {
/*
在线性探测中删除一个节点的话,是不能简单地将一个节点置为null就完事了的。因为在线性探测查找的时候,
如果遍历时遇到一个为null的位置,就可以停止遍历、认定为找不到这个数据了。而如果删除的时候只将这个数据
置为null的话,那么后面的节点就有可能会访问不到。本来存在的数据,但却访问不到,从而出现了问题
所以在这里需要对staleSlot后面的节点做一些处理,这里选择的是rehash的方式
获取哈希槽的位置
*/
int h = k.threadLocalHashCode & (len - 1);
//如果这次获取哈希槽的位置和i不同的话(如果相同就不转移)
if (h != i) {
//就将tab[i]位置的数据清空
tab[i] = null;
//并且重新线性探测一个新的空位置处
while (tab[h] != null)
h = nextIndex(h, len);
//同时把数据转移进去
tab[h] = e;
}
}
}
/*
返回staleSlot位置后第一个为null的哈希槽位置,从本方法的实现中可以看出:本方法只是清理了从staleSlot
到其后不为null的这一段哈希槽的垃圾数据,并不是清理全部哈希槽。清理全部的话需要借助下面的cleanSomeSlots方法
*/
return i;
}
/**
* 第118行、第172行和第191行代码处:
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
//获取下一个哈希槽的位置
i = nextIndex(i, len);
//获取其中的Entry
Entry e = tab[i];
//如果有垃圾数据的话(Entry不为null但是其中的ThreadLocal为null)
if (e != null && e.get() == null) {
//就将n重新置为当前数组的长度,再重新进行do-while循环
n = len;
//删除标志位置为true
removed = true;
//调用expungeStaleEntry方法来删除垃圾数据,删除后会继续循环,直到n等于0为止
i = expungeStaleEntry(i);
}
//n每次循环都会除以2
} while ((n >>>= 1) != 0);
//返回是否删除过垃圾数据的标志位
return removed;
}
/**
* 第119行代码处:
* 是否需要扩容首先还会查看当前全表是否含有垃圾数据,如果有垃圾数据并且删除后还是比数组容量的一半多的话,才进行扩容
*/
private void rehash() {
//全表扫描是否含有垃圾数据,如果有的话就进行删除
expungeStaleEntries();
/*
如果调用上面expungeStaleEntries方法完毕后,size还是大于等于threshold*0.75(也就是数组容量的一半),
就会进行扩容操作
*/
if (size >= threshold - threshold / 4)
resize();
}
/**
* 第299行代码处:
* 从table数组的第一个位置处向后遍历,如果发现有垃圾数据的话(Entry不为null但是其中的ThreadLocal为null),
* 就调用expungeStaleEntry方法进行删除
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
/**
* 第306行代码处:
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//新数组的容量为旧数组的两倍(这里没有使用<<1的方式感觉是因为历史遗留代码的原因)
int newLen = oldLen * 2;
//创建新数组
Entry[] newTab = new Entry[newLen];
int count = 0;
//遍历旧数组上的每一个哈希槽位
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal> k = e.get();
if (k == null) {
/*
如果Entry不为null但是其中的ThreadLocal为null,就说明当前这个是垃圾数据
此时将它的value也置为null,便于GC回收(这里的删除就不需要考虑后续节点的
rehash了,因为所有的节点最后都是要转移到新数组的)
*/
e.value = null;
} else {
//根据新数组容量来进行hash
int h = k.threadLocalHashCode & (newLen - 1);
//通过线性探测的方式来找到要插入的位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
//并且把数据转移进去
newTab[h] = e;
//新数组计数+1
count++;
}
}
}
//走到这里说明完成了数据迁移的过程,此时重新计算一下threshold值
setThreshold(newLen);
//更新一下size
size = count;
//将扩容后新的数组赋值给table
table = newTab;
}
/**
* ThreadLocal:
*/
public T get() {
//获取当前的线程
Thread t = Thread.currentThread();
//获取线程中的threadLocals
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap已经初始化了的话
if (map != null) {
//从threadLocals中寻找对应的Entry
Entry e = map.getEntry(this);
//如果找到的话
if (e != null) {
//直接返回Entry中的value就行了
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
//如果ThreadLocalMap没有初始化,或者没找到Entry的话,就在setInitialValue方法中进行处理
return setInitialValue();
}
/**
* 第12行代码处:
*/
private Entry getEntry(ThreadLocal> key) {
//获取哈希槽的位置
int i = key.threadLocalHashCode & (table.length - 1);
//获取Entry
Entry e = table[i];
if (e != null && e.get() == key)
//如果Entry不为null,并且其中的ThreadLocal也相等的话,就说明找到了,返回这个Entry
return e;
else
//否则未命中的话,就在getEntryAfterMiss方法中进行处理
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal> k = e.get();
if (k == key)
//如果ThreadLocal相等的话,就返回这个Entry
return e;
if (k == null)
/*
如果Entry不为null但是其中的ThreadLocal为null,就说明当前这个是垃圾数据
调用expungeStaleEntry方法来进行删除
*/
expungeStaleEntry(i);
else
//否则通过线性探测的方式来找到下一个哈希槽的位置
i = nextIndex(i, len);
//重新更新一下Entry的指向
e = tab[i];
}
//如果循环走完还找不到的话,就返回null
return null;
}
/**
* 第22行代码处:
*/
private T setInitialValue() {
//获取初始值
T value = initialValue();
//获取当前的线程
Thread t = Thread.currentThread();
//获取线程中的threadLocals
ThreadLocalMap map = getMap(t);
if (map != null)
//如果threadLocals存在,就往其中存放初始数据
map.set(this, value);
else
//如果ThreadLocalMap没有初始化,就完成相关初始化工作并放入初始数据
createMap(t, value);
//最后返回这个初始值就行了
return value;
}
/**
* 第71行代码处:
* 本方法默认返回null,可以由调用者覆写
*/
protected T initialValue() {
return null;
}
/**
* ThreadLocal:
*/
public void remove() {
//获取当前线程中的threadLocals
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//如果不为null就进行删除(删除的是当前的ThreadLocal)
m.remove(this);
}
private void remove(ThreadLocal> key) {
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)]) {
//遍历table数组,如果ThreadLocal相等的话(如果不等说明发生了hash冲突,此时用线性探测的方式找寻下一个哈希槽)
if (e.get() == key) {
//就清除ThreadLocal
e.clear();
//同时尝试删除垃圾数据
expungeStaleEntry(i);
return;
}
}
}
原创不易,未得准许,请勿转载,翻版必究