ThreadLocal使用原理

介绍

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();
}

存入对象时做了几件事情

  1. 以当前角标为起点,往后遍历,如果找到了key值相同的Entry,就直接更新value,没找到就执行2。
  2. 找到一个Entry对象,但是key已经被回收了,证明此对象已经没用了,就将这个旧的对象替换掉。
  3. 以上不满足,证明tab[i]一定没有存入对象,直接创建一个Entry存进去就好。
  4. 最后再清理数组,如果当前数组里面没有需要清除的对象,并且对象数量达到了阈值,就扩容。
替换旧对象
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做了几件事情

  1. 往前遍历数组,看是不是在此时由于gc又出现了一些无效对象,如果有就记录最前面的那个位置,方面后面进行一次性遍历清除。
  2. 往后遍历数组
    1. 看能否找到key相同的Entry,如果有,就更新value,并且顺便清理一次数组里面所有的无效对象。
    2. 如果往前没有无效对象,往后遍历发现了无效对象,就记录以下无效对象的位置。
  3. 上述遍历没有命中,就将当前这个无效对象清理了,顺便复用这个位置。
  4. 最后如果发现当前无效对象后面还有无效的对象,就进行一次清理。

所以,每次在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的数组角标,调整位置重新存储。
  • 20
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值