【源码学习】ThreadLocal 实现原理以及其内部存储结构(神奇的1640531527)

来源:互联网 发布:腾讯云和阿里云的区别 编辑:程序博客网 时间:2024/06/03 19:59

        前言:之前讲到Looper时说到Looper与线程相关,不同线程之间互不干扰,是因为通过prepare创建的Looper对象存放在Looper的一个静态成员sThreadLocal中,由ThreadLocal维护,保证了每个Looper以线程为基础的独立性。 今天和大家一起走读ThreadLocal源码,看一下他究竟是如何做到的。

        ThreadLocal代码量不大一共不超过300行,所以大家不要有压力,耐心读。

=================================

一 举例概述ThreadLocal以及用法

二 ThreadLocal

三 ThreadLocal内部的存储结构ThreadLocalMap

四 ThreadLocalMap存储使用的hash算法以及神奇的1640531527

五 要点总结

=================================

一 举例概述ThreadLocal以及用法

ThreadLocal的用法很简单,直接new对象(支持泛型),通过set、get方法存入和取出数据。ThreadLocal是线程安全的,它存储的值仅属于他所在的线程,线程之间互不影

这里用安卓Looper举例:

sThreadLocal用以保存自身对象:

    // sThreadLocal.get() will return null unless you've called prepare().    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

prepare方法用以初始化创建Looper对象,并使用ThreadLocal保存以保证线程安全:

    public static void prepare() {        prepare(true);    }    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));    }
实际创建了Looper对象调用set存进sThreadLocal。

myLooper方法用以获取Looper对象,实际调用的sThreadLocal.get方法取出:

    public static @Nullable Looper myLooper() {        return sThreadLocal.get();    }

简单写一下Looper的使用:

new Thread(new Runnable() {public void run() {Looper.preper();Looper looper = Looper.myLooper();looper.loop();}}).start();
        我们知道Looper使用单例模式保证对象的唯一性,而获取Looper对象的方法是静态方法,那么他是怎么实现多个线程都能拥有自己的对象的呢?

        事实上,我们在上边的代码可以看到,Looper类中sThreadLocal是唯一且不可变的,而创建对象时判断对象唯一性是判断的 sThreadLocal.get() != null ,Looper对象存在ThreadLocal里,由于ThreadLocal存储对象与线程相关,所以我们在新的线程创建Looper对象是通过ThreadLocal获取,不会受其他线程影响。

那么我们来看ThreadLocal具体是如何做到的。
----------------------------------------------------------------------

二 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)                return (T)e.value;        }        return setInitialValue();    }

        首先获取当前线程t,通过当前线程获取到一个ThreadLocalMap map,可以看出我们要get的数据是从这个ThreadLocalMap里取到的。

看一下getMap取的是个什么东西。

    ThreadLocalMap getMap(Thread t) {        return t.threadLocals;    }
        getMap返回的是Thread的一个成员
    /* ThreadLocal values pertaining to this thread. This map is maintained     * by the ThreadLocal class. */    ThreadLocal.ThreadLocalMap threadLocals = null;
        是ThreadLocal的一个内部类:ThreadLocalMap,他承担了ThreadLocal的存储工作。

        继续往下看,取到map后获取Entry e,返回e.value。这个Entry是ThreadLoacalMap内部的结构,他以ThreadLocal对象为键key,以存储的数据为值value进行存储。

        static class Entry extends WeakReference<ThreadLocal> {            /** The value associated with this ThreadLocal. */            Object value;            Entry(ThreadLocal k, Object v) {                super(k);                value = v;            }        }

继续往下看,当if判断map为null时,return 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);        return value;    }
        这里调用了initialValue()

    /**     * Returns the current thread's "initial value" for this     * thread-local variable.  This method will be invoked the first     * time a thread accesses the variable with the {@link #get}     * method, unless the thread previously invoked the {@link #set}     * method, in which case the <tt>initialValue</tt> method will not     * be invoked for the thread.  Normally, this method is invoked at     * most once per thread, but it may be invoked again in case of     * subsequent invocations of {@link #remove} followed by {@link #get}.     *     * <p>This implementation simply returns <tt>null</tt>; if the     * programmer desires thread-local variables to have an initial     * value other than <tt>null</tt>, <tt>ThreadLocal</tt> must be     * subclassed, and this method overridden.  Typically, an     * anonymous inner class will be used.     *     * @return the initial value for this thread-local     */    protected T initialValue() {        return null;    }

        我们看这个方法的注释,如果开发者想要给一个初始值,可以override该方法,返回初始值。

LocalThread提供给开发者设置初始值的方法 ------ override initialValue


接着来看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);    }
还是取当前线程,根据当前线程拿到它内部的map,如果取到map是空,说明是第一次设置值,map还没有初始化,调用createMap初始化map并将值设置进去:
    void createMap(Thread t, T firstValue) {        t.threadLocals = new ThreadLocalMap(this, firstValue);    }

        如果取到map就set进去。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

三 ThreadLocal内部的存储结构ThreadLocalMap

把ThreadLocalMap拿出来单独看,主要学习一下他是如何处理数据的,可以借鉴:

ThreadLocalMap的构造:

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

1、new一个Entry数组,长度是固定值INITIAL_CAPACITY,是16。也就是说这个容器的初始长度是16。

2、计算第一个元素要存入的位置,threadLocalHashCode位运算&15。我们看一下threadLocalHashCode是如何获得的。

    private static AtomicInteger nextHashCode =        new AtomicInteger();    private static final int HASH_INCREMENT = 0x61c88647;    private final int threadLocalHashCode = nextHashCode();    private static int nextHashCode() {        return nextHashCode.getAndAdd(HASH_INCREMENT);    }
        线程中第一个ThreadLocal的threadLocalHashCode为0,以后每创建一个ThreadLocal,它的threadLocalHashCode会增加一个HASH_INCREMENT增量。这里可以看出threadLocalHashCode其实是用来计算ThreadLocal在table中的存储位置的,threadLocalHashCode是ThreadLocal内final成员,说明每个ThreadLocal的threadLocalHashCode是固定的,有自己特定的位置,也就是说一个ThreadLocal在一个线程中的值是唯一的。

3、以ThreadLocal为键,存储数据为值存放到Entry对象,放到前边定位到的位置上。

4、size用来记录table的大小,后边会讲到,当达到一定容量时,会扩容并重新分配位置。

5、setThreshold设置扩容门限值。


看一下如何通过key获取已有Entry,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); // 直接hash位置未找到,则通过冲突处理算法继续寻找        }
        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); // key为null,无效项,需要清理                else                    i = nextIndex(i, len); // 该位置未找到,则根据冲突处理算法寻找下一位置                e = tab[i];            }            return null;        }

ThreadLocalMap通过set设置值,我们看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)]) {                ThreadLocal k = e.get();// 找到正确位置,赋值退出                if (k == key) {                    e.value = value;                    return;                }// 该位置已有项,但是key为空,说明是无效项,找到我们要找的key的项或新建项赋值,并将该项放入该位置,清理掉无效项                if (k == null) {                    replaceStaleEntry(key, value, i);                    return;                }            }    // 经过循环中的判断筛选最终还未找到目标值,则新建Entry存储,此时的i就是最终筛选出的正确位置。            tab[i] = new Entry(key, value);            int sz = ++size;            if (!cleanSomeSlots(i, sz) && sz >= threshold) // 判断存储量超过门限值,rehash扩容重新分配                rehash();        }

        可以看出,仍然使用的是threadLocalHashCode定位存放位置,循环是为了发生冲突时根据冲突解决算法获得位置,从threadLocalHashCode定位到的位置开始,找到位置则赋值退出,若找到的位置key为空,可能被回收或其他原因,说明是无效值通过replaceStaleEntry清理掉并退出。

冲突算法就是向后+1,查找到末尾则从0计算。

        private static int nextIndex(int i, int len) {            return ((i + 1 < len) ? i + 1 : 0);        }
找到位置则赋值返回,如果查找过程中先发现有无效项,优先将我们的项换到无效项位置并清理无效项:

        private void replaceStaleEntry(ThreadLocal key, Object value,                                       int staleSlot) {            Entry[] tab = table;            int len = tab.length;            Entry e;            // Back up to check for prior stale entry in current run.            // We clean out whole runs at a time to avoid continual            // incremental rehashing due to garbage collector freeing            // up refs in bunches (i.e., whenever the collector runs).            int slotToExpunge = staleSlot;            for (int i = prevIndex(staleSlot, len); // 向前找是否有无效项位置,因为后面清理调用的cleanSomeSlots是向后查找清理无效项的。                 (e = tab[i]) != null;                 i = prevIndex(i, len))                if (e.get() == null)                    slotToExpunge = i;            // Find either the key or trailing null slot of run, whichever            // occurs first            for (int i = nextIndex(staleSlot, len);                 (e = tab[i]) != null;                 i = nextIndex(i, len)) {                ThreadLocal k = e.get();                // If we find key, then we need to swap it                // with the stale entry to maintain hash table order.                // The newly stale slot, or any other stale slot                // encountered above it, can then be sent to expungeStaleEntry                // to remove or rehash all of the other entries in run.                if (k == key) { // 找到我们要找的项,则将我们的查找项与这个无效项换位,清理无效数据项                    e.value = value;                    tab[i] = tab[staleSlot];                    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);        }
        首先向前查找无效项,slotToExpunge标记位置,然后向后查找我们要找的存储位置,期间如果发现有新的无效项并且之前向前查找也没有找到新的无效项,则将此无效项位置赋给slotToExpunge。因为之后的清理动作cleanSomeSlots是向后清理的,我们需要给他一个最早无效项位置。而我们最终找到的目标项位置是最后的查找位置,并且查找到以后会与staleSlot位置互换,所以查找到的目标位置实际是最后的无效项位置,我们需要将在这之前的最早的无效位置找出来,进行向后清理。

继续set方法,循环结束没有找到,说明是新值,新建Entry将其存储起来。新建Entry后会整理存储并检查容量是否超过门限值。首先会通过cleanSomeSlots清理无效位置。

清理无效数据项:

        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) { // 判断是否清理的条件仍然是Entry的key为空。                    n = len;                    removed = true;                    i = expungeStaleEntry(i); // 清理                }            } while ( (n >>>= 1) != 0);             return removed;        }

 
        private int expungeStaleEntry(int staleSlot) {            Entry[] tab = table;            int len = tab.length;            // expunge entry at staleSlot            tab[staleSlot].value = null;            tab[staleSlot] = null;            size--;            // Rehash until we encounter null            Entry e;            int i;            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;                    size--;                } else { // 1 有效值需要重新定位                    int h = k.threadLocalHashCode & (len - 1);                    if (h != i) {                        tab[i] = null;                        // Unlike Knuth 6.4 Algorithm R, we must scan until                        // null because multiple entries could have been stale.                        while (tab[h] != null)                            h = nextIndex(h, len);                        tab[h] = e;                    }                }            }            return i;        }
        清理后按照冲突算法向后循环查找,如果查找到有效项,则需要对此项重新定位,因为此项有可能是根据被清理掉的项按照冲突算法排列的位置,而前边的一项无效项被清理掉了,会导致根据冲突算法已经不能找到该项,所以要重新定位。

如果超出门限值则通过rehash函数扩容并重新排列。

扩容并重排列:

        private void rehash() {            expungeStaleEntries(); // 首先遍历全部清理无效项            // Use lower threshold for doubling to avoid hysteresis            if (size >= threshold - threshold / 4) // 缩小了门限以及时扩容                resize();        }

        rehash首先遍历整个容器以清理无效项,清理后再次判断是否达到门限是否需要扩容。我们发现在整个过程中在扩容钱多次进行无效项清理,为了尽量有效利用空间,尽量不扩容。

        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++;                    }                }            }            setThreshold(newLen);            size = count;            table = newTab;        }

        容量扩大为原来的2倍,并重新根据自己的散列算法将之前的项存入扩容后的容器中。

-----------------------------------------------------------------------------------------------------------------------

四 ThreadLocalMap存储使用的hash算法以及神奇的1640531527

最后说一下用到的散列算法。

前面将到了计算hashcode的增量HASH_INCREMENT = 0x61c88647 十进制为 1640531527,存储容量初始值是16,扩容时以2为底呈指数上升。

    private static AtomicInteger nextHashCode =        new AtomicInteger();    private static final int HASH_INCREMENT = 0x61c88647;    private final int threadLocalHashCode = nextHashCode();    private static int nextHashCode() {        return nextHashCode.getAndAdd(HASH_INCREMENT);    }
key.threadLocalHashCode & (len-1)

hash算法: F(n) = (HASH_INCREMENT*n) % 容量    (n=0,1, ... ,容量-1)

冲突解决:((i + 1 < len) ? i + 1 : 0)

1640531527这是个神奇的数字,

下面是我打印出来的不同容量情况下散列的表现

1---value[0]=0 | 2---value[0]=0 | value[1]=1 | 3---value[0]=0 | value[1]=2 | value[2]=2 | 4---value[0]=0 | value[1]=3 | value[2]=2 | value[3]=1 | 5---value[0]=0 | value[1]=4 | value[2]=4 | value[3]=4 | value[4]=4 | 6---value[0]=0 | value[1]=5 | value[2]=4 | value[3]=5 | value[4]=4 | value[5]=1 | 7---value[0]=0 | value[1]=6 | value[2]=6 | value[3]=4 | value[4]=4 | value[5]=2 | value[6]=2 | 8---value[0]=0 | value[1]=7 | value[2]=6 | value[3]=5 | value[4]=4 | value[5]=3 | value[6]=2 | value[7]=1 | 9---value[0]=0 | value[1]=0 | value[2]=8 | value[3]=0 | value[4]=8 | value[5]=0 | value[6]=8 | value[7]=0 | value[8]=8 | 10---value[0]=0 | value[1]=1 | value[2]=8 | value[3]=1 | value[4]=8 | value[5]=1 | value[6]=8 | value[7]=1 | value[8]=8 | value[9]=9 | 11---value[0]=0 | value[1]=2 | value[2]=10 | value[3]=0 | value[4]=8 | value[5]=2 | value[6]=10 | value[7]=0 | value[8]=8 | value[9]=10 | value[10]=2 | 12---value[0]=0 | value[1]=3 | value[2]=10 | value[3]=1 | value[4]=8 | value[5]=3 | value[6]=10 | value[7]=1 | value[8]=8 | value[9]=11 | value[10]=2 | value[11]=9 | 13---value[0]=0 | value[1]=4 | value[2]=12 | value[3]=4 | value[4]=12 | value[5]=0 | value[6]=8 | value[7]=0 | value[8]=8 | value[9]=12 | value[10]=4 | value[11]=12 | value[12]=4 | 14---value[0]=0 | value[1]=5 | value[2]=12 | value[3]=5 | value[4]=12 | value[5]=1 | value[6]=8 | value[7]=1 | value[8]=8 | value[9]=13 | value[10]=4 | value[11]=13 | value[12]=4 | value[13]=9 | 15---value[0]=0 | value[1]=6 | value[2]=14 | value[3]=4 | value[4]=12 | value[5]=2 | value[6]=10 | value[7]=0 | value[8]=8 | value[9]=14 | value[10]=6 | value[11]=12 | value[12]=4 | value[13]=10 | value[14]=2 | 16---value[0]=0 | value[1]=7 | value[2]=14 | value[3]=5 | value[4]=12 | value[5]=3 | value[6]=10 | value[7]=1 | value[8]=8 | value[9]=15 | value[10]=6 | value[11]=13 | value[12]=4 | value[13]=11 | value[14]=2 | value[15]=9 | 

发现当容量是2的指数时所有的值会填充整个表并且比较均匀的分布。

求助了一下学数学的老同学,分分钟帮我证明该hash算法各项取值的唯一性。

女神亲笔:


实际上对任意奇数都适用,根据上图假设 (j-i) x HASH_INCREMENT= 2^n x (m2-m1) ,j-i<2^n约不掉整个2^n,右侧肯定是偶数,左侧的HASH_INCREMENT只要是奇数,i,j不相等,则必定等式不能成立,说明2^n范围内不存在满足条件的两个不相等的i,j

1640531527 % 16余7,实际上这个散列表是以7为基数自增向后排列的


扩展一下如果不是2的n次方而是其他值:

(j-i) x HASH_INCREMENT= a^n x (m2-m1) 

当HASH_INCREMENT和a^n有公约数时,j-i就有取值能够等于被约掉后的a^n的值,m2,m1取值范围是任意正整数,就有值可以使等式成立,说明会存在重复值。

也就是说只要 HASH_INCREMENT和a^n没有公约数,就可以利用此哈希算法得到一组填充满整个表的散列值。

而本篇ThreadLocal容器扩容是每次扩大1倍,所以使用2^n计算比较简便。



------------------------------------------------------------------------------------------
五 要点总结

在线程中使用ThreadLocal保存数据可以保证在各个线程之间各自的数据是相互隔离的。

ThreadLocal的实际存储容器是自己实现的一个ThreadLocalMap,其对象保存在线程中,也就是说数据实际由线程保存,从而达到了各个线程之间互不干扰,保证线程安全的目的。

ThreadLocalMap使用哈希算法对存储对象排序,其初始容量为16,当达到门限值时会自动扩容并重新排序,容量以2为基数呈指数增长。


        

阅读全文
0 0