ThreadLocal是什么
ThreadLocal提供了线程局部变量,由该类保存的变量,会分开线程,不同的线程会保存不同的变量副本。
import java.util.HashMap;
import java.util.Map;
public class ThreadLocalTest {
ThreadLocal threadLocal = new ThreadLocal();
Map<String, String> map = new HashMap<>(16);
public static final String KEY = "key";
String s1 = "dog";
String s2 = "cat";
public void f1() {
threadLocal.set(s1);
System.out.println("f1通过threadLocal获得的字符串是:" + threadLocal.get());
map.put(KEY, s1);
System.out.println("f1通过map获得的字符串是:" + threadLocal.get());
}
public void f2() {
System.out.println("f2通过threadLocal获得的字符串是:" + threadLocal.get());
System.out.println("f2通过map获得的字符串是:" + map.get(KEY));
threadLocal.set(s2);
map.put(KEY, s2);
System.out.println("f2通过threadLocal获得的字符串是:" + threadLocal.get());
System.out.println("f2通过map获得的字符串是:" + map.get(KEY));
}
public static void main(String[] args) throws Exception {
ThreadLocalTest tlt = new ThreadLocalTest();
new Thread(tlt::f1).start();
Thread.sleep(2000);
System.out.println("-----------------------------------------");
new Thread(tlt::f2).start();
}
}
示例代码很简单,测试了ThreadLocal与map的区别,可以看到结果显示如下:
f1通过threadLocal获得的字符串是:dog
f1通过map获得的字符串是:dog
-----------------------------------------
f2通过threadLocal获得的字符串是:null
f2通过map获得的字符串是:dog
f2通过threadLocal获得的字符串是:cat
f2通过map获得的字符串是:cat
Process finished with exit code 0
结果意味着,ThreadLocal存储的变量是线程独占的,f1方法开始的线程设置的threadLocal变量s1,在f2方法开始的线程中并拿不到。
那我们知道了ThreadLocal保存的变量实际上是线程隔离的。
ThreadLocal之get()方法
我们先从简单的get方法看起,看看ThreadLocal是如何实现线程隔离来获取设置的变量的。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
可以看到原码中获取了当前线程t,又从当前线程中获取到了一个map,有点看不懂啊,这个ThreadLocalMap什么鬼?
我们看看ThreadLocal结构:
可以看到,ThreadLocalMap是ThreadLocal的内部类,而getMap方法呢?
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
很简单,就是从当前线程中拿到一个对象threadLocals,这个对象来自ThreadLocal的内部类ThreadLocalMap
如果map不为空,又去拿map中的entry,如果entry不为空,就把entry的值返回,整个获取过程就在最理想的情况下完成了。
Entry是ThreadLocalMap的内部类,这个类只有一个属性值,还继承了弱引用(作用后面再谈)。
我们看看map.getEntry方法干了些什么事?
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
这里会传入ThreadLocal对象,然后把threadLocal的hash码与(table.length-1)做与运算,拿到Entry数组的下标,这里值得一提的是为啥table的注释会那样写:
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
说这个table的长度必须是2的次方,因为要服务于取下标的【与运算】(&),其运算规则是:
运算规则:0 & 0 = 0; 0 & 1 = 0; 1 & 0 = 0; 1 & 1 = 1;
如果table的长度是2的次方,那么table.length-1的二进制码就会是:1111···,比如:
table.length-1 | 二进制码 |
-1 (其值为7) | 111 |
-1 (其值为15) | 1111 |
-1 (其值为31) | 11111 |
做与运算的时候,结果才会均匀分布。因为如果二进制码是0,不论与什么(0或1)运算都是0,其结果唯一,不具有均匀分布特性。
拿到下标后,取出table中第i个元素,如果该元素不为空并且该元素取出的引用(e.get()方法的返回值就是),额,这里需要看看Refrence类的源码:
public abstract class Reference<T> {
//从英文注释看出来,该referent根据引用类型不同会被GC区别对待
private T referent; /* Treated specially by GC */
public T get() {
return this.referent;
}
/* -- Constructors -- */
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
//省略其他代码
}
其实get()方法无非就是返回当前类的一个泛型对象。而这个类跟我们说的源码的关系就在于Entry继承了WeakRefrence,而WeakRefrence又是Refrence的子类。
Refrence类设计出来的目的就是让咱们自定义的类A继承它,相当于给咱们的类A穿上一层外套,方便垃圾收集器识别何时应该对类A的对象进行回收。
-----------------------------------------------------------------------补充知识开始-----------------------------------------------------------------------
1、对象的引用类型有强软弱虚四种
强引用:A a = new A(); 普通new对象,就是强引用
软引用:new SoftReference(new A()),当内存不够用时,优先收集软引用所占用的内存
弱引用:new WeakReference(new A()),每当发生GC,都会收集该引用指向对象所占用的内存(ThreadLocal的Entry用到了)
虚引用:new PhantomReference(new Object (),QUEUE),垃圾回收也是见一次回收一次,但是回收后,有一个通知到其队列里,用来控制堆外内存回收用。就是当这个引用指向的对象被回收时,虚引用的队列里有一个通知,应该是指向系统内存的引用,再用c语言之类的底层语言回收堆外内存
2、reference指引用本身,referent指的是被包裹的对象(谁继承上图的类,谁就被包裹)
-----------------------------------------------------------------------补充知识结束-----------------------------------------------------------------------
我们前面说到:Entry是ThreadLocalMap的内部类,这个类只有一个属性值,还继承了弱引用,现在说说继承弱引用的作用:
方便每次GC都把Entry对象都给回收掉。
ThreadLocal的结构图表明,Entry是ThreadLocal内部的静态内部类ThreadLocalMap内部的静态内部类,我们看看源码:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//省略代码无数
}
源码表明Entry其构造方法调用了super(k),也就是Refrence的构造方法,完成对refrent的赋值。也就是前面e.get()方法的返回值。
那好,既然Entry的构造传入的ThreadLocal的对象k,那么e.get()方法取出来,也应该是ThreadLocal类的一个对象。
回到getEntry的源码:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
拿到下标后,取出table中第i个元素,如果该元素不为空并且该元素取出的引用(ThreadLocal的一个对象k)也是与ThreadLocal的对象key相等,那么就返回table的第i个元素。
如果e.get()取出的引用与传入的ThreadLocal对象key不相等,那么说明,有可能发生了垃圾回收,弱引用遇到垃圾回收,是像老鼠过街,过一次,被收拾一次。如果发生了垃圾回收,ThreadLocal的get()方法就会调用getEntryAfterMiss方法。
我们看看getEntryAfterMiss方法的源码:
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
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)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
可以看到,getEntryAfterMiss方法传入的参数有计算好的下标,以及可能为空的e = table[i](或者e.get()取出的值不是当前对象,e.get()此时多半是null)。
1、如果e = table[i]为空,就直接返回为null,表明真的没有该Entry,即总的来说,ThreadLocal中没有该线程存储的私有值。
2、如果e = table[i]不为空,尝试从e.get()方法中取,如果这次判断与k==key(上面有分析),就返回该entry;如果e.get()取出的ThreadLocal变量不为空,又不与传入的key相等,数据过时了(垃圾回收删除了引用),应该获取下一个下标,直到找到当前线程存入的那个key为止。
而nextIndex的实现就简单了:
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
就是判断当前获取的下标i的下一个是不是小于table的长度,如果小于table的长度,就获取下一个下标,超过table的长度就用0。这里可以看出,Entry数组逻辑上是环形结构。
3、最后,取出table[i](代码中的e=tab[i],这里的i已经自增,变了哟),赋值给e,当下一次循环的时候,再次经过上述流程的判断,直到找到相同的key,把table[i]返回。
4、最麻烦的一点,如果e.get()方法获取的弱引用为空,但是此时的e=table[i]不为空,表示有gc活动清除了弱引用,这时应该把所有的老旧的引用都清除,重新为该entry建立新的弱引用。
虽然,ThreadLocal获取线程的私有变量副本看起来不需要参数,但是其内部实现是取出了当前线程对象,利用了当前线程对象的属性threadLocals(ThreadLocalMap类),获取threadLocals的内部类entry对象时,设定了参数this(当前ThreadLocal的对象引用),那实际上是内部的键值存储就是:
键 | 值 |
ThreadLocal当前对象引用(this) | 存入的Obj |
现在梳理下threadLocal.get()的流程:
ThreadLocal之set(T value)
往ThreadLocal里加入线程隔离的私有数据,会用到ThreadLocal的set方法,因为线程隔离,所有set也没有显式的键。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
前面两行代码都与获取一致,获取到当前线程中的定义在ThreadLocal中的ThreadLocalMap,为空则创建,不为空,就设值。
我们先来看看简单的createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
就是new了一个ThreadLocalMap赋值给当前线程的threadLocals变量。注意,这个指的是ThreadLocal当前对象。结构上面我们上面展示的表格。
再看看map.set(this,value)干了啥:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//利用key计算table的下标i,均匀分布原理如上述【与运算】分析
int i = key.threadLocalHashCode & (len-1);
//hash冲突走for循环
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
//取出该entry的弱引用
ThreadLocal<?> k = e.get();
//k未被GC删除,并且与key能匹配上,覆盖存值,返回
if (k == key) {
e.value = value;
return;
}
//k是空的情况,弱引用被清除
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//hash不冲突走这边
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
set分析一:hash不冲突的情况
当hash不冲突的时候,把value放到了一个new出来的Entry里面,在赋值给Entry数组table。
接下来,set方法做了一件事,即:判断是否能清理掉一些table中已经被GC回收的弱引用,如果发现了有Entry存在,但是其弱引用被清除的,那么cleanSomeSlots返回true,不会执行rehash方法。
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);//n决定了扫描的次数
return removed;
}
while循环即使n传入的值是16,也只循环四次,所有大多数情况,该cleanSomeSlots方法只会循环3到4次(n>>>1 与 n=n/2取整,差不多)。
假如i算出来是8,n是4,那么在逻辑上,n的变化是4-->2-->1-->0,会循环三次,会查找table的9、10、11的entry是否存在以及是否其弱引用被删除。
这时候有两种情况:
1、8号后面仨,肚子装了entry,弱引用也被清除了,这时候set是不会触发rehash的
2、8号后面三,可能entry为空,可能不为空但是弱引用还在(e.get()!=null),这时候cleanSomeSlots返回false,再判断出当前table中如果size大于2/3的table容量时,会触发rehash
在cleanSomeSlots方法中的while循环中,如果ThreadLocal在set时计算的当前下标i对应的下一个下标的table[i]有entry元素,并且其弱引用已被清除,也就是:
if (e != null && e.get() == null)
这个条件满足,那么就会执行expungeStaleEntry方法,清理掉过时的Entry,这里是直译的stale,我的理解是Entry的弱引用遇到了GC,当entry的弱引用被删除,该entry就称为stale entry。
这里必须看看expungeStaleEntry方法的源码了:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清除数组table中给定下标的元素
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 循环table中的元素,从给定的staleSlot的下一个元素开始,遇到null就rehash
Entry e;
int i;
for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//糟糕,下一个元素的弱引用也被清除了
if (k == null) {
// 那就也清除数组table中给定下标的元素
e.value = null;
tab[i] = null;
size--;
} else {//下一个元素的弱引用未被清除的情况
//重新给该元素计算下标
int h = k.threadLocalHashCode & (len - 1);
//计算的下标不重样的话
if (h != i) {
//原下标的元素置为空
tab[i] = null;
//反复查询新下标的值是否有元素占用
while (tab[h] != null) {
//占用了就取下一个
h = nextIndex(h, len);
}
//没占用就把e赋值到table新下标的位置中
tab[h] = e;
}
}
}
return i;
}
源码中可以知道,expungeStaleEntry的方法不仅删除给定下标的元素,连带着该下标的后续下标也会受到检查,如果也是stale entry,那么会被清除掉,如果不是stale entry就会被重新计算下标再赋值到table中(这步操作不明其意图,如果有看官大佬研究透了,欢迎留言调教),最后会返回【不是连带清除么,清除到哪个位置了?】的下标。
set分析二:hash冲突的情况
源码这么多,您记得住?我可记不住,再看看说到set方法哪里了:
冲突的时候也分两种情况:
1、table中的i下标位置虽然有entry,但是i的nextIndex的弱引用与当前key一致,就进行覆盖操作
2、i的nextIndex的弱引用被清除,那么就执行replaceStaleEntry方法
stale entry即上述的【我的理解是Entry的弱引用遇到了GC,当entry的弱引用被删除,该entry就称为stale entry】,从方法名来看,是替换掉stale entry,我们来一探究竟:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 往table数组的给定下标的前面找,只要该位置有值,弱引用被清除,就记录下该位置
// 重复找,slotToExpunge的位置只记录往前找到的最后一个
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 往table数组的给定下标的后面找,只要不为空
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果k和key相等,就替换掉k中的entry的value为新传入的value
// 并且把table中的staleSlot和staleSlot的下一个交换
if (k == key) {
//e是过时槽的下一个槽,先附上值value
e.value = value;
//下一个槽保存了过时槽的数据,等待被删除
tab[i] = tab[staleSlot];
//过时槽装入过时槽下一个槽的数据,交换完成,可以清除过时槽的entry了
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
replaceStaleEntry方法弊端是:会清除给定下标i附近所有的连续的stale entry,如下图:假如staleSlot(也就是某个下标)是4,那么2、3、4、5、6都会被清除掉
至此,ThreadLocal的源码算是大部分看完了。打个哈欠,睡觉。