美文网首页
jdk1.8ThreadLocal底层源码详解

jdk1.8ThreadLocal底层源码详解

作者: IT界刘德华 | 来源:发表于2019-10-26 23:58 被阅读0次

    ThreadLocalMap结构

    /**
             * 数组初始容量
             */
            private static final int INITIAL_CAPACITY = 16;
    
            /**
             * 节点数组
             */
            private Entry[] table;
    
            /**
             * 数组里的entry数量
             */
            private int size = 0;
    
            /**
             * 阈值,扩容阈值
             */
            private int threshold; // Default to 0
    

    底层源码解析

    set方法分析
    //threadLocal的set方法
    //其实就是对threadLocal的内部类threadLocalMap类赋值,将它看成map
    //threadLocalMap的内部类entry可以看成一组组的key-value
    public void set(T value) {
            //获取当前线程,因为线程内部持有threadLocalMap
            Thread t = Thread.currentThread();
            //获取当前线程的ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
    • 当前map是null的话,则创建一个新的map,然后做一些初始化设置
     void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                //INITIAL_CAPACITY默认16
                table = new Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }
    
    • 当前线程存在map了 那就开始对当前线程的map进行一系列的扫描清理,然后设值
      当前set方法大体流程图如下
    //对当前key进行设值
    private void set(ThreadLocal<?> key, Object value) {
    
                
                Entry[] tab = table;
                int len = tab.length;
                //对当前key算落入下标位置
                int i = key.threadLocalHashCode & (len-1);
    
                //对tab里面的脏数据进行扫描
                //直到碰到entry==null 停止
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
    
                    //如果已经存在相同key,用新值覆盖旧值,停止方法运行
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    
                    //有脏key进行回收和替换,并且扫描周边的脏key,停止方法运行
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
                
                //没有脏key并且没有找到相同key则执行下方方法
                //新建entry
                tab[i] = new Entry(key, value);
                int sz = ++size;
                //cleanSomeSlots都没标记到周边的脏key,并且tab的length已经大于等于2/3len,就开始全表清理脏key,当大于等于1/2len才会开始扩容
                //因为可能有空key占用空间所以为了减少数据插入停滞则以2/3len为阈值
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    
    • 在进行向后扫描的时候,如果找到和当前要设的key相同的key,则直接用新值覆盖,如果找到的key==null,也就是脏key,则replaceStaleEntry进行替换清理,并且做一系列的周边扫描
    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                           int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
                Entry e;
    
                //staleSlot表示key==null的下标,默认将找到的key==null的下标作为要开始扫描的下标赋给了slotToExpunge
                int slotToExpunge = staleSlot;
                //向前查找找到更前面有key==null的脏数据重写赋给slotToExpunge,直到碰到tab[i]的entry是null停止
                //因为entry为空key说明那一格插槽没有脏数据了
                for (int i = prevIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = prevIndex(i, len))
                    if (e.get() == null)
                        slotToExpunge = i;
    
                //经过上一个for循环,已经定位到了最前一个脏数据的下标
                //下面这个循环则往后查找脏数据下标,直到遇到空的entry停止
                for (int i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) {
                    ThreadLocal<?> k = e.get();
    
                    //如果下一个key和要设置的key相同
                    if (k == key) {
                        //对这个entry的值进行赋值
                        e.value = value;
                        //交换位置的值,把陈旧的entry赋值给next的tab
                        tab[i] = tab[staleSlot];
                        //则陈旧的entry用现在要插入的entry赋值,这样做的目的是可以把空key的entry往后挪扫描
                        tab[staleSlot] = e;
    
                        //只会进一次,也就是第一个for循环没找到脏数据,第二个for循环第一次key==k,后面也不会进去
                        if (slotToExpunge == staleSlot)
                            slotToExpunge = i;
                        //expungeStaleEntry清除空key数据,从slotToExpunge开始,里面还会清除附近的脏key
                        //cleanSomeSlots则是对刚才expungeStaleEntry清除停止的下标进行一个扩大范围的扫描清除,以n>>>1进行扫描清除
                        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                        return;
                    }
    
                    //往后扫碰到的key是null并且(向前查找的for循环没有定位到更前面的脏数据),则将要清理的slotToExpunge往后移
                    //只会进一次,也就是第一个for循环没找到脏数据,第二个for循环第一次key==null,后面也不会进去
                    if (k == null && slotToExpunge == staleSlot)
                        slotToExpunge = i;
                }
    
                //陈旧的staleSlot值清除,方便GC回收
                tab[staleSlot].value = null;
                //如果第二个for循环没进入if (k == key) ,也就是没有设置过一样的key,则创建新的entry放到陈旧的下标上
                tab[staleSlot] = new Entry(key, value);
    
                //上方进入if (k == null && slotToExpunge == staleSlot) 一次后,清除下标则向后挪了一位,因为之前脏数据位已经被新的entry占用,所以不必清除staleSlot这位
                //expungeStaleEntry清除空key数据,从slotToExpunge开始,里面还会清除附近的脏key
                //cleanSomeSlots则是对刚才expungeStaleEntry清除停止的下标进行一个扩大范围的扫描清除,以n>>>1进行扫描清除
                if (slotToExpunge != staleSlot)
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            }
    
    • expungeStaleEntry方法是对数据进行一个真正意义上的清理。具体就是对陈旧的值进行置空清理,容量减一。然后再开始扫描周边有没有脏key,如果有直接清理掉,如果key不是空,并且算出来的数组下表和旧的下标不一致,说明因为hash冲突引起的不断寻找空tab插入,所以会进行重新计算它的新位置,将旧位置的值清掉,然后再新位置上不断往后挪直到找到空地可插位置插入,则完成脏key的清理流程
    
            private int expungeStaleEntry(int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
    
                // expunge entry at staleSlot
                //陈旧的值置为空,方便GC回收
                tab[staleSlot].value = null;
                tab[staleSlot] = null;
                //值为空后,数组容量减一
                size--;
    
                Entry e;
                int i;
                //开始循环清理该陈旧值的后方旁边其它空值,知道碰到数组里面没有entry为止,因为没有entry说明还没创建对象,有entry没有key才是软引用过期的脏数据
                for (i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) {
                    ThreadLocal<?> k = e.get();
                    //说明已经被回收,脏数据,val没被回收,所以这里收到置值为null
                    if (k == null) {
                        e.value = null;
                        tab[i] = null;
                        size--;
                    } else {
                        
                        int h = k.threadLocalHashCode & (len - 1);
                        //hash算完下角标跟它取出来的位置不一样说明是因为它算出来的那个值已经hash冲突,导致它往后插入而引起不一样
                        if (h != i) {
                            //帮当前位置这个值置空掉,将之前数据赋值给新算出的下标位置
                            tab[i] = null;
    
                            //如果算出来的位置又有entry了,再找下一个,直到找到空的位置将entry设置进去。对该entry,重新进行hash并解决冲突
                            while (tab[h] != null)
                                h = nextIndex(h, len);
                            tab[h] = e;
                        }
                    }
                }
                //返回的是循环清理的最后一个值,也就是碰到数组entry为空的前一个entry的下角标值
                return i;
            }
    
    • cleanSomeSlots方法主要对脏key进行一个扩大范围的扫描,然后对脏key进行标记,由expungeStaleEntry处理掉,对n进行右移,如果有脏key则标记removed清除,然后调用expungeStaleEntry开始清理脏key,直到n==0,这里并没有全部清除而是类似于折中清除,提高性能
    //清空一些插槽的空key,但是这个方法并没做真正清理
    //而是对空key进行标记,在expungeStaleEntry进行清除
    //(n >>>= 1) != 0这个是控制log2n查找,并没有全部查找,在性能和脏数据之间做了一个折中
    //只要找到一个值,又重新给n赋值,又开始log2n直到,n == 0 为止抽的这些都没有脏数据
    //扩大扫描范围
    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];
                    //有key被回收,脏数据,则进行expungeStaleEntry清理
                    if (e != null && e.get() == null) {
                        //调用cleanSomeSlots方法的时候的n跟现在的数组的长度可能会不一样,所以这是扩大扫描范围
                        n = len;
                        removed = true;
                        i = expungeStaleEntry(i);
                    }
                } while ( (n >>>= 1) != 0);
                //只要查找的对象entry的key不为空,也就是没有一个脏数据,返回值就是false,也就是没有移除数据
                return removed;
            }
    
    • 重新计算hash,当然在要扩容之前要对
    //扩容,对hash进行重新计算
    private void rehash() {
                //全表脏key清理
                expungeStaleEntries();
    
                //当容量大于等于1/2len开始扩容
                if (size >= threshold - threshold / 4)
                    resize();
            }
    
    • 全表数据清理
    //对tab的全表进行一个数据清理
    //因为调用这个方法是在扩容,扩容前进行全表清理
    private void expungeStaleEntries() {
                Entry[] tab = table;
                int len = tab.length;
                for (int j = 0; j < len; j++) {
                    Entry e = tab[j];
                    if (e != null && e.get() == null)
                        //数据清理
                        expungeStaleEntry(j);
                }
            }
    
    • 扩容,当前容量达到旧的长度的一半,则开始扩容,容量变成原来的两倍,然后旧表的脏key的值置null,如果不是脏key则开始往新的表转移,重新计算hash取新数组索引。然后再重新设置阈值和容量
    //扩容,为原理的2倍
    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 {
                            //重新计算hash,新数组下标值
                            int h = k.threadLocalHashCode & (newLen - 1);
                            while (newTab[h] != null)
                                h = nextIndex(h, newLen);
                            newTab[h] = e;
                            count++;
                        }
                    }
                }
                //设置新阈值
                setThreshold(newLen);
                //设置新容量
                size = count;
                //将新数组替换旧数组
                table = newTab;
            }
    
    • 设置阈值
    //设置阈值
    private void setThreshold(int len) {
                threshold = len * 2 / 3;
            }
    
    get方法
    • 流程图
    • 源码解析
    public T get() {
            Thread t = Thread.currentThread();
            //获取map
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                //获取entry节点
                ThreadLocalMap.Entry e = map.getEntry(this);
                //entry节点不是空说明已经创建过了,直接从entry获取值返回就可以
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            //map是空或者map不是空entry节点是空会在这里进行初始化
            return setInitialValue();
        }
    
    //获取entry节点
    private Entry getEntry(ThreadLocal<?> key) {
                //计算数组索引位置
                int i = key.threadLocalHashCode & (table.length - 1);
                Entry e = table[i];
                //key和当前位置的key一样,就直接返回entry
                if (e != null && e.get() == key)
                    return e;
                //可能因为hash冲突导致插入的值不在计算位置上
                //依次往后查找,并标记清除脏key
                else
                    return getEntryAfterMiss(key, i, e);
            }
    
    //可能因为hash冲突导致插入的值不在计算位置上
    //依次往后查找,并标记清除脏key
    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;
           }
    
    //如果都没找到对应entry和值,则对当前线程进行一个初始化
    //里面的方法和set类似,就不做过多解释了
    private T setInitialValue() {
            //没有子类重写的话为null
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
        }
    
    remove
    //threadLocal数据清除
    public void remove() {
            //获取map
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 //调用localmap的remove方法
                 m.remove(this);
         }
    
    private void remove(ThreadLocal<?> key) {
                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对象是同一个,就清除引用对象,并开始调用expungeStaleEntry清理陈旧的值以及周边的值
                    //expungeStaleEntry这个方法已经在set里解析过了,这边就不多说
                    if (e.get() == key) {
                        //就是把reference指向null,也就是将该entry的key指向null。方便后面对该entry进行清理
                        e.clear();
                        expungeStaleEntry(i);
                        return;
                    }
                }
            }
    

    总结

    代码其实并不难,唯一的难点在set方法线性探测法的扫描,脏key的处理,从看源码到画流程图还是挺不容易的,喜欢的朋友可以点点赞,哈哈哈!!!

    相关文章

      网友评论

          本文标题:jdk1.8ThreadLocal底层源码详解

          本文链接:https://www.haomeiwen.com/subject/dfvlvctx.html