ThreadLocal,从名字上可以知道和线程本地有关系,这个类会为每个线程提供属于线程自己的局部变量。ThreadLocal可以通过initialValue()为每个线程赋值,也可以由线程自己调用ThreadLocal的set()进行赋值。每个线程操作对应的变量时,与其他线程不会发生冲突,改动仅对自己可见。总的来说,ThreadLocal适用于变量在线程间隔离的场景,这里也可以看出ThreadLocal采用了空间换时间的策略保证并发安全。
在jdk1.7和1.8中,ThreadLocal原理并不相同,本文主要分析jdk1.8的。
首先来看一个demo,ThreadLocal为主线程和t1线程分配id,并且在initialValue()中分配一次就让id++。
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){
int id=0;
@Override
protected Integer initialValue() {
return id++;
}
};
new Thread(()->{
System.out.println("t1线程获取数据:"+threadLocal.get());
},"t1").start();
System.out.println("主线程获取数据:"+threadLocal.get());
}
在这个demo中我们重写了initialValue()方法,让每个线程都能拿到初始值,如果不重写这个方法,就需要线程自己调用set()方法向里面存值。我们从set()方法开始看起。
ThreadLocal的常用方法
set()流程
//value:每个线程调用set存的值
public void set(T value) {
//获取当前线程的引用
Thread t = Thread.currentThread();
//获取线程保存的ThreadLocalMap
//ThreadLocalMap是ThreadLocal的静态内部类,是真正用来存放数据的
ThreadLocalMap map = getMap(t);
//map是否被创建过
if (map != null)
//创建过,就调用map.set()
map.set(this, value);
else
//第一次调用时,map并未创建,所以走这个逻辑
createMap(t, value);
}
//getMap,就是获取每个线程自己保管的ThreadLocalMap
// ThreadLocal.ThreadLocalMap threadLocals = null; 这句代码在Thread类中
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//创建ThreadLocalMap,并赋值给当前线程的threadLocals字段
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
到这里set流程就结束了,可以知道在jdk1.8中,ThreadLocalMap是由线程进行保管的。
get()流程
public T get() {
//获取当前线程引用
Thread t = Thread.currentThread();
//获取到线程保管的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//ThreadLocalMap的静态内部类Entry,数据存放在这里
ThreadLocalMap.Entry e = map.getEntry(this);
//判断Entry是否为null
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
//返回数据
return result;
}
}
//map为null,说明还未调用set(),就调用get()
return setInitialValue();
}
private T setInitialValue() {
//调用initialValue(),如果用户有重写就调用重写的方法,否则是空实现
T value = initialValue();
//以下是set()的逻辑,最后返回初始化的值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
get()的逻辑也比较简单,就是通过线程保管的ThreadLocalMap里面查找数据并返回,如果Map还未创建,也会先执行初始化逻辑。
remove()流程
public void remove() {
//获取线程保管的map
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//调用map的remove移除数据
m.remove(this);
}
到这里ThreadLocal常用的方法就分析完毕了,可以发现ThreadLocal仅仅是对ThreadLocalMap的操作进行了封装,最核心的逻辑还是在ThreadLocalMap内完成的,接下来就去看它的源码。
ThreadLocalMap源码分析
ThreadLocalMap就是ThreadLocal的内部类,也在ThreadLocal文件中。首先来看一下它的重要字段,方法,和构造。
重要的字段
//散列表的初始长度
private static final int INITIAL_CAPACITY = 16;
//ThreadLocalMap内的散列表
private Entry[] table;
//当前散列表的元素个数
private int size = 0;
//扩容阈值
private int threshold; // Default to 0
重要的方法
//设置扩容阈值
private void setThreshold(int len) {
//扩容阈值为当前散列表长度的2/3
threshold = len * 2 / 3;
}
//返回当前下标的前一个下标
//i:下标 len:散列表长度
private static int prevIndex(int i, int len) {
//i-1>=0 说明当前元素下标>=1,返回i-1
//i-1<0 说明当前元素下标已经为0了,在数组头部,那就返回数组末尾的下标,形成一个环式查找
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
//同上,返回当前下标的下一个下标,到数组尾就返回0
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
构造
//firstKey:在前面创建map的时候,会将ThreadLocal对象传入
//firstValue:ThreadLocal对应的值
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建长度为16的散列表
table = new Entry[INITIAL_CAPACITY];
//寻址算法 threadLocalHashCode & 长度-1
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//将ThreadLocal和值封装成Entry放入散列表中
table[i] = new Entry(firstKey, firstValue);
//设置容量
size = 1;
//设置扩容阈值 数组长度*2/3=10
setThreshold(INITIAL_CAPACITY);
}
在构造中涉及到了threadLocalHashCode和Entry,现在我们来分析一下这两个知识点。
ThreadLocalHashCode
HashCode相关代码在ThreadLocal类中
//当前ThreadLocal对象的HashCode,非静态,说明每个ThreadLocal对象独有
private final int threadLocalHashCode = nextHashCode();
//为每一个ThreadLocal对象分配HashCode,静态,说明ThreadLocal对象共享
private static AtomicInteger nextHashCode =
new AtomicInteger();
//黄金分割数,每分配一个HashCode,就需要增加这个数,可以使hash算法分散的更均匀
private static final int HASH_INCREMENT = 0x61c88647;
//返回HashCode
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
Entry
Entry是ThreadLocalMap的静态内部类,用来构建key-value
//WeakReference 弱引用,如果当前对象只有弱引用指向时,gc会回收这个对象
//这里的key ThreadLocal就是弱引用,如果key被回收后,就为null,对应的value是强引用,会造成内存泄漏的问题
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
关于ThreadLocal内存泄露的问题,可以去看看这篇文章,本文不解释概念了。
测试ThreadLocal 在gc后引发的threadLocalMap的key为null,但value不为null的情况
在上面这篇文章的代码里,debug会输出一些其他值,请将上文42行的输出语句改为下面的即可
System.out.println("弱引用key:"+referenceField.get(o)+",值:"+valueField.get(o));
到这里我们可以大概清楚了ThreadLocal的结构
线程Thread内部持有ThreadLocalMap的引用,Map内部使用Entry数组进行保存以ThreadLocal为key的k-v键值对Entry对象。
接下来我们就去看看ThreadLocalMap的相关操作方法
set()流程
private void set(ThreadLocal<?> key, Object value) {
//散列表
Entry[] tab = table;
//长度
int len = tab.length;
//计算当前ThreadLocal存放的下标
int i = key.threadLocalHashCode & (len-1);
//e=tab[i] 将当前桶位的Entry赋值给e
//e!=null 说明当前桶位已经有数据了,发生了hash冲突
//e = tab[i = nextIndex(i, len)] 每一轮循环后,线性的向后查找
//ThreadLocalMap和HashMap不同,HashMap解决hash冲突的方法是链地址法,而ThreadLocalMap是线性探测
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//获取当前Entry的key ThreadLocal对象
ThreadLocal<?> k = e.get();
//如果传入的key和当前桶位的ThreadLocal对象相同
if (k == key) {
//说明是替换操作
e.value = value;
return;
}
//k==null 说明了什么
//Entry不为null,而key为null,说明这是个过期数据,key已经被gc回收了,而value没有释放
if (k == null) {
//传入当前桶位下标和k-v,去替换当前过期数据
replaceStaleEntry(key, value, i);
return;
}
}
//退出循环,说明找到了合适的桶位,创建Entry放入对应的桶位。
tab[i] = new Entry(key, value);
//设置容量
int sz = ++size;
//cleanSomeSlots 启发式清理,后面分析
//!cleanSomeSlots(i, sz)成立,说明没有清理到过期数据
//sz >= threshold成立,说明需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//staleSlot:是一个过期Entry的下标
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
//获取散列表
Entry[] tab = table;
//获取散列表数组长度
int len = tab.length;
//临时变量
Entry e;
//表示开始探测式清理过期数据的开始下标。默认从当前 staleSlot开始。
int slotToExpunge = staleSlot;
//以当前staleSlot开始 向前迭代查找,找有没有过期的数据。for循环一直到碰到null结束。
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len)){
//过期数据指的是Entry不为null,而key为null
if (e.get() == null){
//说明向前找到了过期数据,更新探测清理过期数据的开始下标为i
//只有找到了过期数据才slotToExpunge
slotToExpunge = i;
}
}
//以当前staleSlot向后去查找,直到碰到null为止。
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//获取当前元素 key
ThreadLocal<?> k = e.get();
//条件成立:说明是替换
if (k == key) {
//替换新数据。
e.value = value;
//交换位置的逻辑
//将table[staleSlot]这个过期数据放到当前循环到的table[i]这个位置,table[i]就是e
tab[i] = tab[staleSlot];
//将tab[staleSlot] 中保存为当前entry。就是将i和staleSlot的元素进行交换
tab[staleSlot] = e;
//条件成立:
// 1.说明replaceStaleEntry 一开始时 的向前查找过期数据 并未找到过期的entry.
// 2.向后检查过程中也未发现过期数据
if (slotToExpunge == staleSlot)
//开始探测式清理过期数据的下标修改为当前循环的i
slotToExpunge = i;
//cleanSomeSlots :启发式清理
//expungeStaleEntry:探测式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//条件1:k == null 说明当前遍历的entry是一个过期数据.
//条件2:slotToExpunge == staleSlot 说明向前查找过期数据并未找到过期的entry.
if (k == null && slotToExpunge == staleSlot)
//因为向后查询过程中查找到一个过期数据了,更新slotToExpunge 为当前位置。
//前提条件是前驱扫描时未发现过期数据
slotToExpunge = i;
}
//如果执行到这里
//说明向后查找过程中并未发现 k == key 的entry,说明当前set操作是添加
//直接将新数据添加到 table[staleSlot]对应的桶位
//因为调用此方法处就判断当前桶位是过期数据,所以先清空value
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//条件成立:除了当前staleSlot以外 ,还发现其它的过期桶位,所以要开启清理数据的逻辑
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
因为ThreadLocal是一个弱引用,会引发内存泄漏的问题,所以在设计ThreadLocal时,内部就已经做好了两种过期数据回收,这个会在后面提到。在set()的流程中可以发现,如果发生了hash冲突,就会向当前桶位的后面线性的查找空桶位进行存放数据,这种方式被称为线性探测。
get()流程
在ThreadLocal里,get()会调用到这个方法里,所以我们从这里开始分析
private Entry getEntry(ThreadLocal<?> key) {
//计算下标
int i = key.threadLocalHashCode & (table.length - 1);
//获取当前桶位的Entry
Entry e = table[i];
//如果Entry不为null,并且key相同
if (e != null && e.get() == key)
//返回
return e;
else
//进入这里,说明发生过hash冲突,当前要查询的ThreadLocal对象被放在了其他的桶位
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//向后遍历数组,直到碰到空Entry为止
while (e != null) {
//遍历到的Entry的ThreadLocal
ThreadLocal<?> k = e.get();
if (k == key)
//相同就返回
return e;
if (k == null)
//Entry!=null,k==null 过期数据
//探测式过期数据回收
expungeStaleEntry(i);
else
//继续向后查询
i = nextIndex(i, len);
e = tab[i];
}
//说明没有该数据
return null;
}
get()的逻辑还算简单,计算下标后线性查询,并且碰到过期数据就进行探测式回收。
remove()流程
接下来是remove的逻辑
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)]) {
//查找到对应的Entry
if (e.get() == key) {
//将Entry的key设置为null
e.clear();
//从当前下标开始进行探测式清理
expungeStaleEntry(i);
return;
}
}
}
remove也很简单,线性查找到后,从当前桶位开始探测式清理。
扩容流程
在set方法里还有一个rehash()方法没有分析,这里就设计到map的扩容流程了。
private void rehash() {
//过期数据回收
expungeStaleEntries();
//如果清理完后的容量>=扩容阈值的3/4
if (size >= threshold - threshold / 4)
//进行扩容
resize();
}
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);
}
}
private void resize() {
//获取当前散列表
Entry[] oldTab = table;
//获取旧表的长度
int oldLen = oldTab.length;
//新表长度为旧表长度的2倍
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) {
//说明是一个过期数据,释放value
e.value = null; // Help the GC
} else {
//计算该元素在新表的下标
int h = k.threadLocalHashCode & (newLen - 1);
//可能发生了hash冲突,所以要找到一个合适的位置存放
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
//计算数量
count++;
}
}
}
//设置下一次的扩容阈值
setThreshold(newLen);
//设置容量
size = count;
//将新表赋值给table
table = newTab;
}
扩容的流程也是比较简单的,但是有没有朋友看到这会想,在多线程环境下,扩容为什么没有加锁呢?其实很简单,这个map是由线程自己保管的,所以扩容是一个单线程的逻辑。
到这里ThreadLocalMap的常用方法就分析完成了,接下来就该分析探测式清理和启发式清理的逻辑。
探测式清理
探测式清理其实也比较好理解,和线性探测思想一样,遍历散列表清理过期数据。
//staleSlot:从调用处看,为过期数据的下标
private int expungeStaleEntry(int staleSlot) {
//获取散列表
Entry[] tab = table;
//散列表长度
int len = tab.length;
//因为是过期数据,将value和Entry都设置为null
tab[staleSlot].value = null;
tab[staleSlot] = null;
//清理了当前过期数据,容量-1
size--;
Entry e;
int i;
//i = nextIndex(staleSlot, len) 从过期数据开始,向后遍历
//(e = tab[i]) != null 直到遍历桶位为null,即table[i]==null
for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//过期数据处理
if (k == null) {
//清空
e.value = null;
tab[i] = null;
//容量-1
size--;
} else {
//说明这里是正常的数据
//因为前面可能清理了一部分过期数据,导致前面的桶位为null,这时要解决hash冲突带来的影响
//ThreadLocalMap使用线性探测来解决hash冲突,导致有些桶位的数据会有一定的偏移
//所以在这里重新计算下标,找到一个最接近hash算法计算出来的桶位进行存放
//计算下标
int h = k.threadLocalHashCode & (len - 1);
//i是当前下标
//成立:说明当前的Entry发生过冲突
if (h != i) {
//将当前桶位设置为null
tab[i] = null;
//从计算的下标开始遍历,找到合适的桶位进行存放
while (tab[h] != null)
h = nextIndex(h, len);
//有可能是h,也有可能是h到i中间的某个位置
tab[h] = e;
}
}
}
//退出循环,下标为i的桶位就是table[i]==null
return i;
}
探测式清理过程也比较好理解,从过期数据开始向后遍历查找过期数据,如果碰到正常数据,就重新计算下标,减少hash冲突带来的影响。
启发式清理
//i:从调用处来看,下标为i的桶位一定不是过期数据,启发式清理从i后面开始
//n:有可能为当前元素个数,有可能为当前散列表长度,这里按长度16来举例
private boolean cleanSomeSlots(int i, int n) {
//是否清理过数据
boolean removed = false;
//散列表
Entry[] tab = table;
//散列表长度
int len = tab.length;
do {
//获取i下一个桶位
i = nextIndex(i, len);
Entry e = tab[i];
//如果是过期数据
if (e != null && e.get() == null) {
//将n设置为数组长度
n = len;
//清理过数据,所以设置为true
removed = true;
//从当前桶位开始再进行一次探测式清理
i = expungeStaleEntry(i);
}
//n=16
// 16 >>> 1 =8
// 8 >>> 1 =4
// 4 >>> 1 =2
// 2 >>> 1 =1
// 1 >>> 1 =0
//当n=16时,会进行5次搜索,如果找到过期数据,会从过期数据开始进行一次探测式清理
} while ( (n >>>= 1) != 0);
return removed;
}
到这里,关于ThreadLocal的一些重要方法就已经分析完毕了,还没有看懂的朋友,请打开源码,跟着流程一起走一遍,有些地方需要自己动手画一画才知道这些代码做了哪些操作。