最近在研究EventBus的时候碰到一个ThreadLocal的使用场景,考虑到Handler里面也用到了这玩意,比较重要和高端,所以研究下,先来看个Demo:
package testthreadlocal;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadLocalDemo {
//一个可以用原子方式更新的int值
private static final AtomicInteger ai = new AtomicInteger(0);
//ThreadLocal对象
static ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return ai.getAndIncrement();
};
};
public static void main(String[] args) {
//创建5个线程,每个线程都有一个id和他绑定,注意,这个id是我们
//强行和他绑定的一个int数据而已,并不是系统为这个线程分配的id
for (int i = 0; i < 5; i++) {
new Thread() {
public void run() {
System.out.println("线程" + Thread.currentThread().getName()
+ " 的ID是:" + local.get());
};
}.start();
}
}
}
上面的例子中首先创建了一个ThreadLocal,这个ThreadLocal存储的是一个Integer类型的数据;然后简单的调用了AtomicInteger的getAndIncrement方法对ThreadLocal进行了初始化;接着创建了5个线程,每个线程都可以自由访问这个ThreadLocal;最后5个线程都去ThreadLocal取里面存储的Integer的值,然后输出结果如下:
线程Thread-0 的ID是:1
线程Thread-2 的ID是:2
线程Thread-3 的ID是:3
线程Thread-4 的ID是:4
线程Thread-1 的ID是:0
可以看到,对于同一个成员变量local里面存储的值,不同的线程获取的结果不一样,有没有感觉好神奇?local看起来非常简单,在创建的时候就是简单的调用了初始化函数:initialValue;都没有set方法,然后get出来的值就不一样了,这么厉害的吗?要想分析这种现象的原因,就必须研究ThreadLocal的代码,不啰嗦了,123,上源码,先从initialValue方法开始分析:
//默认的initialValue方法的实现就是返回空
//在上例中,是将AtomicInteger原子自增
protected T initialValue() {
return null;
}
initialValue方法很简单,实际上这个方法是根据我们的需求进行重写的,下面分析get方法,这是重点:
public T get() {
//获取线程,就是调用get方法,在上例中,我们创建了5个子线程,每
//个子线程都调用了get,那么这个t就分别指向了刚刚创建的5个子线程
Thread t = Thread.currentThread();
//ThreadLocalMap是一个自定义的HashMap,不过他没有继承自HashMap
//而是自己实现了一个,他的作用仅仅是保存线程本地变量,getMap就是获
//取跟线程绑定的ThreadLocalMap,Thread类里面有这样的成员变量;每
//个Thread的子类在创建并初始化的时候就会给这个变量赋值
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap不为空,那么进入if获取对应线程的值
if (map != null) {
//根据ThreadLocalMap获取Entry,
//注意哦,传进去的是TheadLocal
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果ThreadLocalMap为空,或者Entry为空
//那么进入setInitialValue去设置初值
return setInitialValue();
}
通过get方法,我们可以得到如下的图:
ThreadLocal3
可以看到,每个线程都有一个ThreadLocalMap对象,这是一个Map,键是ThreadLocal,值是我们要存储的值。get方法的第一步就是拿到线程的Map;然后根据传进来的ThreadLocal对象去Map里面找Entry,如果找到了,那么拿到此Entry的value,这个value正是我们要获取的值;如果没有拿到Entry,那么调用setInitialValue去初始化一个Entry,并返回初始的value值。下面分析setInitialValue方法
下面首先来分析getEntry方法:
private Entry getEntry(ThreadLocal<?> key) {
//获取此key的hashcode,并与entry数组的长度 - 1做
//与操作,其实质跟HashMap中获取桶的索引是一样一样的
int i = key.threadLocalHashCode & (table.length - 1);
//拿到数组元素Entry
Entry e = table[i];
//如果数组元素不为空,而且key一样,那么返回此元素
if (e != null && e.get() == key)
return e;
else
//如果元素不存在,或者存在,但是和目标key不一样
//也就是说此线程的Map并没有保存传进来的ThreadLocal
return getEntryAfterMiss(key, i, e);
}
getEntry方法比较简单,就是根据key的hash和数组长度 - 1做与运算算出此key的数组索引(这个key是ThreadLocal对象),注意,这里ThreadLocal的hash是通过一个原子自增的int型数据来实现的。索引拿到了,那么数组元素自然就得到了。如果此数组元素Entry不为空,而且此Entry的key和目标key一样,那么直接拿到这个Entry的value返回即可;如果Entry为空,或者Entry不为空但是此Entry的key和目标的key不一样,那么直接调用getEntryAfterMiss,下面分析这个方法:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
//拿到数组和长度
Entry[] tab = table;
int len = tab.length;
//如果元素存在,但是key不一样,可能存在hash碰撞,也有可能是
//key为空;如果是hash碰撞,那么使用探针法一个个比对数组元素
while (e != null) {
//调用Entry的get方法获取ThreadLocal值
ThreadLocal<?> k = e.get();
//如果此ThreadLocal等于传进来的,那么返回
if (k == key)
return e;
//如果此key为null,那么擦除这个Entry
//因为key是弱引用,很容易被干掉;如果
//被干掉了,那么对应的value也要被干掉
if (k == null)
expungeStaleEntry(i);
else
//如果key不为空,那么去
//数组的下个元素查找值
i = nextIndex(i, len);
e = tab[i];
}
//如果探针法还没找到元素Entry
//说明此ThreadLocal压根就没有
//被保存进此线程的Map,那么返空
return null;
}
getEntryAfterMiss方法也比较简单,进入这个方法的前提是在ThreadLocalMap里面没有找到key和目标key一样的Entry。发生此现象的原因有三种:
1.Entry压根就不存在,也就是说,Map里面没有保存此ThreadLocal,这种情况对应上面代码的最后一行:return null;
2.Entry存在,key也存在,但是key不一样,说明发生了hash碰撞,这个时候就用探针法一个个去拿到数组的元素比对,这种情况对应上面代码的i = nextIndex(i, len);
3.Entry存在,但是key为空,这就说明Entry里面存储的ThreadLocal会回收了,因为Entry里面的key是一个ThreadLocal弱引用,当ThreadLocal被回收时,key就为空了,这时候就要把这整个Entry擦除掉,因为留着他也没有意义了。
下面看下擦除Entry的方法expungeStaleEntry:
//擦除value的操作,参数staleSlot代表第几个数组元素
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
//擦除元素,并将数组元素个数自减,
//因为key已经为null了,所以无需操作key
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
//当遇到key是空的情况,需要重新hash;因为数组
//元素的内存必须是连续的,一旦擦除一个元素,那么
//此数组元素的内存就不连续了,重新hash就是为了保
//这种内存的连续性
Entry e;
int i;
//往死里遍历此数组
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//拿到key值
ThreadLocal<?> k = e.get();
if (k == null) {
//如果碰到key为空的情况,那么
//将相应的value和Entry置空
e.value = null;
tab[i] = null;
size--;
} else {
//重新计算Entry的索引
int h = k.threadLocalHashCode & (len - 1);
//如果新的索引和老的索引不一样,
//那么将老索引对应的Entry置空
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
//如果新索引的Entry不为空,那么使用
//探针法让新索引指向新索引的下一个索引
while (tab[h] != null)
h = nextIndex(h, len);
//最后确定新的索引后,将Entry放进去
tab[h] = e;
}
}
}
return i;
}
expungeStaleEntry方法的思路是根据索引拿到数组的元素Entry,然后把这个Entry的value和他本身都置空;置空后,数组元素的内存就不连续了,此时从置空的那个索引开始遍历后面的元素,如果又发现key为空的情况,那么继续擦除Entry,方法同上;碰到key不为空的情况,重新计算这个Entry的索引,如果老的索引和新的索引不一样,那么将此Entry放入新的索引,如果新的索引本来就有元素Entry了,那么继续使用探针法查找新的索引,一直到找到为止,然后将此Entry放进去。
通过上面几个方法的分析,我们知道了ThreadLocal的get方法里面的Entry是怎么拿到的,玩意这个Entry没拿到怎么办?这种情况就说明此ThreadLocal是第一次放入此线程的ThreadLocalMap中,那么就调用setInitialValue进行初始化,下面看看:
//从命名来看,是设置初始值
private T setInitialValue() {
//调用initialValue,这个方法是需要自
//己重写的,上例中就是将ai进行了原子自增。
T value = initialValue();
//获取调用get方法的线程
Thread t = Thread.currentThread();
//根据线程获取ThreadLocalMap,也就是
//获取Thread内部的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap,那么调用set方法设置具体的值;一般
//来说,在线程调用init方法的时候,都会对此Map进行初始化
if (map != null)
map.set(this, value);
else
//如果map实在不存在,那么创建此Map
//注意两个参数,一个是当前线程,另一个是value
createMap(t, value);
//将设置的值返回回去
return value;
}
可以看到,setInitialValue的作用就是首选调用initialValue整出一个初始值来,这个值是要放入线程的ThreadLocalMap的;然后获取此线程的ThreadLocalMap;如果ThreadLocalMap不为空,那么调用他的set方法将ThreadLocal和setInitialValue算出来的值当做一个键值对放进此ThreadLocalMap;如果此ThreadLocalMap为空,那么调用createMap创建一个ThreadLocalMap,在创建的时候就将键值对传进去,createMap和getMap方法非常简单,不单独分析。至此,ThreadLocal的get方法分析完毕,下面用图形来表示这个过程:
ThreadLocal4
有取就有存,下面分析下set方法,看看ThreadLocal是怎么存储数据的:
public void set(T value) {
//首先拿到调用set的线程
Thread t = Thread.currentThread();
//根据线程,拿到此线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果Map不为空,直接set
if (map != null)
map.set(this, value);
//如果Map为空,那么创建
else
createMap(t, value);
}
ThreadLocal的set方法本身是很简答的,他的实现思路是:
1.首先获取调用线程。
2.其次获取此线程的ThreadLocalMap。
3.如果ThreadLocalMap不为空,那么直接set
4.如果为空,那么创建Map,创建的时候就把ThreadLocal和value传进去保存了。
看得出,ThreadLocalset方法的核心还是ThreadLocalMap的set方法,下面分析:
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
//首先拿到ThreadLocalMap的元素数组
Entry[] tab = table;
//数组长度
int len = tab.length;
//根据key(ThreadLocal类型)计算此键值对应该存放的索引
int i = key.threadLocalHashCode & (len-1);
//从上面计算出来的索引开始遍历数组
//因为可能产生hash碰撞,此时需要指
//针探测,所以需要遍历数组
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//拿到Entry的key,也就是ThreadLocal类型的对象
ThreadLocal<?> k = e.get();
//如果此Entry的key和目标的key一样
//那么直接更新这个Entry的值就好了
if (k == key) {
e.value = value;
return;
}
//流程执行到这里,意味着发生了hash碰撞
//遍历过程中,发现存在key为空的Entry,那么需要擦除他
//擦除完了会退出for循环,因为在擦除过程中会进行for循环
//,遍历i后面的数组元素,直到找到key相同的Entry,或者
//没找到,此时就会创建新的Entry插入到这个key为空的位置
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果没发现此key,也没有key为null的Entry,
// 那么创建一个Entry存进ThreadLocalMap
tab[i] = new Entry(key, value);
//数组数量自增
int sz = ++size;
//清除key为空的Entry,如果没有这样的Entry,但是数组个
//数有超出了阈值,那么调用rehash进行扩容,容量是原来两倍
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
LocalThreadMap的set方法的思路是:
1.根据key算出数组索引。
2.以算出来的索引为起点,向后遍历数组
3.如果碰到key相同的Entry,那么更新值并返回
4.如果碰到key为空的Entry,那么调用replaceStaleEntry并返回
5.如果3和4都没碰到,那么创建一个全新的Entry并插入数组里面
6.如果走的是5流程,那么清理可能存在key为空的Entry;而且如果数组满了,那么调用rehash进行扩容
下面分析replaceStaleEntry方法:
//从命名都能看出,这是替换废弃的Entry,所谓的废弃的Entry,是指key为null的Entry
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
//拿到数组和数组长度
Entry[] tab = table;
int len = tab.length;
Entry e;
//数组索引,这个索引上的Entry的key为空,同时要以这
//个索引为起点,清理此索引后面所有的key为空的Entry
int slotToExpunge = staleSlot;
//以这个索引的前一个索引为起点往前遍历此数组
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//如果发现此Entry的key为空,那么将索引赋值给
//slotToExpunge;也就是说清理的起点发生了变化
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
//继续遍历数组,以key为空的Entry的索引的下一个索引为起点,向后遍历
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果找到key相同的Entry,那么就替换值
if (k == key) {
e.value = value;
//索引为staleSlot的Entry的key是空的,这里把那个key为空
//的Entry移到遍历到的索引,同时将遍历到的Entry放入那个key
//为空的数组索引里面,说白了就是交换数组里面的元素
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
//如果两个值相等,说明传进来的索引的前面没有key为null的Entry
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//这个时候就需要擦除key为空的Entry了,不过如果传进来的索引
//的前面有key为空的Entry,那么从前面的索引为起点开始清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果在传进来的索引的后面又发现了空key的Entry,
//但是前面没有,那么重新记录清理的起点
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
//如果遍历一圈没找到key相同的Entry,说明是第一次存储此
//ThreadLocal,那么新建一个Entry存入传进来的索引里面
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
//两个值不等,说明传进来的索引的前面或者后面有key为空的
//Entry,那么以slotToExpunge为起点,清理key为空的Entry
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
}
首先注意调用replaceStaleEntry方法的前提,那就是在遍历数组的过程中,发现有一个Entry的key为空,那么将此Entry的索引和要存储的键值对传进去,这个方法的设计思路如下:
1.记录key为空的索引为slotToExpunge,意思是这个索引后面的数组元素都要遍历一遍,以便清除key为空的Entry。
2.以传进来的索引的前一个索引为起点,遍历数组,看看有没有key为空的Entry,如果有,那么将其索引赋值给slotToExpunge,这意味着清理的起点变了。
3.以传进来的索引的后一个索引为起点,遍历数组,看看有没有key和传进来的key相同的Entry,如果有,那么首先更新此Entry的值;然后将传进来的那个key为空的Entry存入这个位置,最后将这个Entry存入传进来的那个位置。如果传进来的前面没有key为空的Entry,那么更新清理的起点为当前的索引;最后以slotToExpunge为起点开始清理key为空的Entry,然后返回。
4.如果遍历了一圈发现没有找到key和传进来的key一样的Entry,那么创建一个新的Entry,将他存放到传进来的那个索引里面
如果遍历一圈,发现没有key相同的Entry,怎么办?那么就创建一个Entry,放到上图第一个数组key为空的那个索引里面即可。以上就是set方法的执行过程。
可以看到,set方法是把值set到了每个线程中,get方法是从每个线程的TheadLocalMap中获取值,ThreadLocal本身只是拿到线程的TheadLocalMap,然后通过这个Map去get和set,ThreadLocal的实现核心还是每个线程的TheadLocalMap。
除了get和set方法,还有remove方法,remove方法特别简单,首先查找key相同的Entry,然后调用他的clear方法将数据清空,接着调用expungeStaleEntry方法清理数组,上面都分析过,不单独分析了。
网友评论