介绍
ThreadLocal可以在线程内部保存一组变量,这些变量只有当前线程自己可以访问,其它线程无法访问,避免线程竞争。它也提供了一种方案,使得线程内多个方法间不用繁琐的传递上下文参数,仅需要在使用的方法内通过ThreadLocal的get方法就能拿到上下文。
使用方式
简单的例子
public static ThreadLocal<String> sThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
sThreadLocal.set(3);
System.out.println(sThreadLocal.get());//输出3
}).start();
new Thread(() -> {
System.out.println(sThreadLocal.get());//输出null
}).start();
}
可以看到,使用相同的对象threadLocal,在不同的线程里面操作后得到的结果互不干扰。
源码解析
get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
...
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
首先通过Thread.currentThread(),拿到当前线程对象,再拿到线程内部的ThreadLocal.ThreadLocalMap变量(线程独有),通过ThreadLocalMap以当前的ThreadLocal对象为key,拿到一个Entry对象,Entry里面保存了两个变量:ThreadLocal为key,T为value。再通过Entry拿到里面的泛型对象value。
总结两点
- ThreadLocal.ThreadLocalMap是存在每个线程里面的,所以每个线程拿到的都是他自己的map。
- ThreadLocal对象可以只有一个,但在不同的ThreadLocalMap里面,哪怕key(ThreadLocal对象)一样,拿到的value也是不一样的,做到了线程隔离。
这里可以看到,如果map为null就会通过setInitialValue方法拿到并返回一个value,看一下setInitialValue方法
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
初始化
首先通过initialValue拿到value,initialValue是ThreadLocal内的一个protected方法,默认返回是null。
protected T initialValue() {
return null;
}
当我们需要定义一个初始值的时候,可以通过继承ThreadLocal类,来重写它的initialValue方法,或者通过以下方式实现
public static ThreadLocal<String> sThreadLocal = ThreadLocal.withInitial(() -> "123");
可以看到withInitial方法内部就是new了一个ThreadLocal的子类SuppliedThreadLocal来实现的
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
创建ThreadLocalMap
当map没有被初始化过,会通过createMap来创建一个ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
看下ThreadLocalMap的构造方法,这里比较有意思
public class ThreadLocal<T> {
...
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
...
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap {
...
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
...
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
}
首先初始化了Entry数组,大小为2的4次方,接着拿到ThreadLocal的threadLocalHashCode这个值和tab数组长度-1进行与操作,得到了一个数组的下标i,然后创建一个Entry对象,将传入的ThreadLocal作为key,传入的firstValue作为value,存入table数组角标i的位置。
碰撞避免和解决
threadLocalHashCode是ThreadLocal类的成员变量,意味着每创建一个ThreadLocal对象threadLocalHashCode就会被初始化,且上一个对象创建后被累加的静态变量nextHashCode在下一次创建对象时又被继续累加,且每次都会累加0x61c88647,所以每一个被创建的ThreadLocal里面threadLocalHashCode的值都是不一样的(全局累加)。
0x61c88647可以让生成出来的值较为均匀的分布在2的幂大小的数组中。啥意思呢,上面我们可以看到table的大小初始化为16,当超过阈值进行resize后,大小会变为原大小的2倍,即2的5次方,2的N次方-1后得到的值转换为二进制一定是N个1。比如2的4次方-1=15转换为2进制为00001111,0x61c88647与00001111相与得到的都将是这个数的低N位。ThreadLocalMap就是把这个值当成数组下标,而0x61c88647比较神奇,可以让这个下标均匀分布,减少下标冲突产生。写个列子
public class TestMain {
private static final int HASH_INCREMENT = 0x61c88647;
public static void main(String[] args) {
hash(16);//输出7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
hash(32);//输出7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
}
private static void hash(int size){
int hashCode = 0;
for(int i=0;i<size;i++){
hashCode = i*HASH_INCREMENT+HASH_INCREMENT;
System.out.print((hashCode & (size-1))+" ");
}
System.out.println();
}
}
可以看到,当长度为16或32,for循环每一次生成的hashCode都不一样。如果以此hasCode为角标去存储数据,它会均匀命中所有角标,最终存满整个数组。所以,当我们每次创建一个ThreadLocal对象,计算生成的数组角标都不会有冲突,每个Entry对象都会存放在合适的位置。
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);
}
拿到当前线程的ThreadLocalMap,如果map不为null就将value设置进去,看下map.set()方法:
private void set(ThreadLocal<?> key, Object value) {
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)]) {
//如果key就是Entry里面的key,那就代表之前存过
if (e.refersTo(key)) {
//直接更新最新value就好
e.value = value;
return;
}
//如果这个Entry的key被回收了
if (e.refersTo(null)) {
//就替换旧的对象
replaceStaleEntry(key, value, i);
return;
}
}
//i会在上面for循环里面赋值,到了这里,就代表tab[i]为null,直接new一个Entry放在这里就好
tab[i] = new Entry(key, value);
//同时把存入的对象数量++
int sz = ++size;
//当没有需要清除的无用对象且存入的对象数量已经达到阈值
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//就扩容重新调整数组对象角标
rehash();
}
存入对象时做了几件事情
- 以当前角标为起点,往后遍历,如果找到了key值相同的Entry,就直接更新value,没找到就执行2。
- 找到一个Entry对象,但是key已经被回收了,证明此对象已经没用了,就将这个旧的对象替换掉。
- 以上不满足,证明tab[i]一定没有存入对象,直接创建一个Entry存进去就好。
- 最后再清理数组,如果当前数组里面没有需要清除的对象,并且对象数量达到了阈值,就扩容。
替换旧对象
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
//往前遍历
//可以假设当前数组情况如下:staleSlot为2,第0个Entry恰好在此时由于gc回收弱引用变为了null
//命中key为传入的key
//[{null,v},{k,v},{null,v},{null,v},{命中key,v},null,...,null]
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.refersTo(null))
//slotToExpunge此时被赋值为0,这里之所以要往前遍历找到第一个key为null的Entry,是为 //了在后续调用expungeStaleEntry时,可以从头开始遍历,一次性清理所有无效Entry
slotToExpunge = i;
//往后遍历
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
if (e.refersTo(key)) {
//找到命中key
e.value = value;
//交换两个对象,为了保存hash table的order,这点不是很明白
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//先调用expungeStaleEntry,以角标0为起始点,开始往后清理
//再调用cleanSomeSlots进行扫描清理
//清理完成后就可以返回了
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果tab[i]前面没有无效对象,自己又是无效的对象,那就以自己为起始,方便后面进行顺序清理
//此时数组情况[{null,v},{k,v},{null,v},{null,v},{命中key,v},null,...,null]
//i = 3;
if (e.refersTo(null) && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//把tab[2]={null,v}的value清空
tab[staleSlot].value = null;
//再创建一个Enrty放在这里,既清理了旧对象,又复用了这个位置
tab[staleSlot] = new Entry(key, value);
//slotToExpunge初始与staleSlot是相等的,都是2,这里不相等,证明在往后遍历数组时,发现角标2后 //面还有无效对象需要进行清理,就以后面发现的第一个无效对象开始往后顺序清理。
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
replaceStaleEntry做了几件事情
- 往前遍历数组,看是不是在此时由于gc又出现了一些无效对象,如果有就记录最前面的那个位置,方面后面进行一次性遍历清除。
- 往后遍历数组
- 看能否找到key相同的Entry,如果有,就更新value,并且顺便清理一次数组里面所有的无效对象。
- 如果往前没有无效对象,往后遍历发现了无效对象,就记录以下无效对象的位置。
- 上述遍历没有命中,就将当前这个无效对象清理了,顺便复用这个位置。
- 最后如果发现当前无效对象后面还有无效的对象,就进行一次清理。
所以,每次在set数据的时候,都会清理数组里面已经无效的Entry对象,get方法也一样会清理,这里就不展开讲了。那啥时候Entry对象会无效呢?
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry的key,ThreadLocal对象是被弱引用持有的,这样当外部没有任何强引用指向这个ThreadLocal对象时,触发gc后,Entry的key就可能变为null,这样就会判定此Entry是一个无效对象。可以看到,key是弱引用,但value是强引用,无法自动回收,所以才会在每次get和set方法中,主动的去清除value的引用。
rehash扩容
既然使用了数组来存放,就会涉及到扩容的问题,ThreadLocalMap主要是在set时判断存储对象数量是否达到阈值,然后调用了resize()方法进行扩容。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//扩容后长度变为原来的两倍
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) {
e.value = null; // Help the GC
} else {
//以新的数组长度,重新计算角标
int h = k.threadLocalHashCode & (newLen - 1);
//如果冲突就找下一个空位进行赋值
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
//将当前存储对象数量++
count++;
}
}
}
//设置新的扩容阈值newLen*2/3
setThreshold(newLen);
size = count;
table = newTab;
}
扩容方法比较简单,主要遍历数组重新计算每个Entry对象的角标,调整位置重新存放,减少后续set冲突。以上就介绍完了ThreadLocal,主要是ThreadLocalMap的存取原理。
使用场景
在Android里面,ThreadLocal使用最多的就是用于Looper的获取,我们知道,每个线程只能有一个Looper,因为当调用Looper的loop方法后,会进入死循环,无法调用到loop外部的代码,同一个线程不可能有多个死循环一起执行。
public final class Looper {
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
public static void loop() {
final Looper me = myLooper();
...
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
}
可以看到sThreadLocal是Looper里面的静态全局变量,当调用Loop.prepare()后,会创建一个Looper对象,通过ThreadLocal的set方法存放在当前线程的ThreadLocalMap里面。在调用Looper.loop()方法后,又通过ThreadLocal将当前线程ThreadLocalMap存入的looper取出来,两个方法调用根本不用传递looper对象,非常方便。也能保证在不同线程里面拿到的looper对象都是自己的,互不干扰。
总结
主要介绍了ThreadLocal的存取原理
- ThreadLocal主要用于存放线程局部变量,避免与其它线程同时使用临界资源导致竞争问题。
- ThreadLocal是通过每个Thread存放的ThreadLocal.ThreadLocalMap进行数据的存取的。
- ThreadLocal.ThreadLocalMap会以ThreadLocal对象为key,并通过0x61c88647的叠加与上数组长度-1,得到一个不容易冲突的数组角标,进行存放。
- ThreadLocal.ThreadLocalMap持有的ThreadLocal对象为弱引用,当不再有强引用指向它时,Entry对象会被标记为无效。
- ThreadLocal的每次get和set都会清理无效的Entry对象。
- 当ThreadLocal.ThreadLocalMap里面存储的Entry对象达到阈值,会进行扩容,并重新计算每个Entry的数组角标,调整位置重新存储。