zoukankan      html  css  js  c++  java
  • ThreadLocal源码

    Thread和ThreadLocal的关系

    初始化ThreadLocalMap和弱引用Entry
    set方法与哈希冲突
    清理槽
    get方法也会清理槽
    扩容
    手动清理的重要性

    Thread和ThreadLocal的关系

    每个Thread中都持有一个ThreadLocalMap的实例,ThreadLocalMap是ThreadLocal的内部类。当Thread中没有ThreadLocalMap则需要先实例化ThreadLocalMap.

    public class Thread implements Runnable {
        ThreadLocal.ThreadLocalMap threadLocals = null;//该对象是ThreadLocal中的内部类ThreadLocalMap
    }
    
    public class ThreadLocal<T> {
        //计算出来的hash值用它来确定Entry存放到哪个哈希槽
        private final int threadLocalHashCode = nextHashCode();
        //这是个固定值
        private static final int HASH_INCREMENT = 0x61c88647;
        //这个默认值是0,但new ThreadLocal后断点看到的值不是0,这是因为这是一个静态成员,在我们自己创建ThreadLocal前,main方法会先加载ThreadLocal给这个赋值了。
        private static AtomicInteger nextHashCode = new AtomicInteger();
        //每次调用该方法都会在原有的nextHashCode值上加上0x61c88647
        private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
        
        //设置值
        public void set(T value) {
            Thread t = Thread.currentThread();//获取当前线程。
            ThreadLocalMap map = getMap(t);//获取当前线程的成员变量ThreadLocal.ThreadLocalMap threadLocals 
            if (map != null)
                map.set(this, value);//如果当前线程中的ThreadLocalMap已经实例化则set
            else
                createMap(t, value);//如果当前线程中的ThreadLocalMap没有实例化则实例化。
        }
        
        //在这走实例化ThreadLocalMap
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    }

    初始化ThreadLocalMap和弱引用Entry

    ThreadLocalMap里最重要的属性是Entry[],这个数组的初始长度是16,扩容阈值是size*2/3,Entry是ThreadLocalMap的内部类,Entry继承了弱引用。Entry里的key是ThreadLocal,value是设置的值。如果ThreadLocal栈引用结束了,在发生GC时虽然Entry还持有ThreadLocal的引用,这个ThreadLocal也会被垃圾回收,所以ThreadLocalMap常常伴随着扩容,清理操作。

    static class ThreadLocalMap {
        //继承WeakReference很重要,WeakReferences是弱引用,在每次GC后都会回收弱引用对象里的引用值(若通过可达性分析查到引用值没有其他可达的Root,则会回收)
        //这个Entry就构成了唯一的key,也就是ThreadLocal。value是ThreadLocal.set(parameter)的参数
        static class Entry extends WeakReference<ThreadLocal<?>> {
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);//最终传递给了Reference中的referent
                    value = v;
                }
        }
        
        //ThreadLocalMap中的容器,一个线程持有一个ThreadLocalMap就相当于持有了一个Entry数组
        private Entry[] table;
        
        //数组的初始容量
        private static final int INITIAL_CAPACITY = 16;
        
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];//实例化数组
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//确定数组的位置
                //初始化ThreadLocalMap不会出现hash冲突。
                table[i] = new Entry(firstKey, firstValue);
                //已有元素++
                size = 1;
                //计算扩容阈值
                setThreshold(INITIAL_CAPACITY);
        }
        
        private void setThreshold(int len) {
                //初始化的容量第一次扩容的阈值是10,也就是说在数组的size是10的情况下就会触发扩容。
                threshold = len * 2 / 3;
        }
      }
    }

    set方法与哈希冲突

    ThreadLocal的set方法是使用ThreadLocalMap的set方法。他分为四种情况。1 计算哈希后确定的槽内是null没有Entry表示没有哈希冲突,此时new一个Entry放入槽内。 2 计算哈希后确定的槽内有Entry但是槽内的Entry的key和当前的ThreadLocal相同则直接替换value。

    3 计算哈希后确定的槽内有Entry但是key和当前ThreadLocal并不是同一个,则表示哈希冲突,此时顺着数组往右寻找,直到碰到有Entry但是没有key的槽,这表示这个槽内曾经有过ThreadLocal但是被GC掉了,此时这个槽是个废槽,可以替换掉Entry。 4 哈希冲突后向右

    并没有找到被GC的槽,此时只能是找到距离最近的一个槽内没有Entry的,创建一个Entry存入。

    static class ThreadLocalMap {
      //顺着当前下标往后查询。如果查询到了数组末尾则返回0号下标
        private static int nextIndex(int i, int len) {
                return ((i + 1 < len) ? i + 1 : 0);
        }
        
        //顺着当前下标往前查询。如果已经是0则返回数组末尾下标
        private static int prevIndex(int i, int len) {
                return ((i - 1 >= 0) ? i - 1 : len - 1);
        }
        
        private void set(ThreadLocal<?> key, Object value) {
                //拿到数组
                Entry[] tab = table;
                //数组长度
                int len = tab.length;
                //hash&length-1 效果类似hash%length
                int i = key.threadLocalHashCode & (len-1);
                //在这就要处理hash冲突了。如果hash值不冲突,那么算出来的index位置的Entry肯定是null.那么不会进入循环。
                //如果进入了循环,有没有可能两个if都不满足,有可能。这表示hash值冲突了,但是不是同一个ThreadLocal,并且hash值相同的槽内的ThreadLocal没有被GC。
                //那么只能是一直找到Entry是null的位置,然后跳出循环。
                for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
                    //如果是第一次循环到这里进去了,表示是同一个ThreadLocal多次设置值。则直接替换值。情况2
                    if (k == key) {
                        e.value = value;
                        return;
                    }
                    //如果ThreadLocal为null则表示发生了GC把弱引用ThreadLocal清理了。
                    //需要将当前set的key和value放入这个废掉的槽内,并且看看有没有需要清理的槽。情况3
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
                //没有进入循环,或者从循环跳出了。如果没有进入循环则i就是hash&length-1的位置表示当前算出来的hash值没有冲突,也是第一次使用。情况1
                //如果是循环跳出来的,则这个i就是hash&length-1.算出来的位置向后移动循环次数的位置。表示hash冲突了,并且冲突后的槽往后也都没有被GC
                //只能是往后顺延找别的可用槽。总之会找到一个在数组内Entry为空的位置。创建Entry放进数组。情况4
                tab[i] = new Entry(key, value);
                //已有元素++
                int sz = ++size;
           //如果没有清理槽,并且当前长度已经大于等于了阈值则扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } //走到这个方法表示通过hash&length-1的位置上的Entry中的key是null或者是哈希冲突后,往数组后查询发现有Entry中的key是null private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) { //数组 Entry[] tab = table; //数组长度 int len = tab.length; Entry e; //Entry为null的哈希槽 int slotToExpunge = staleSlot; //从Entry为null的哈希槽位置向前找,一直找到Entry为null停止 for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len)){ //在向前寻找的过程中标记Entry中key为null的下标 if (e.get() == null) slotToExpunge = i; } //从Entry为null的哈希槽位置向后找,一直找到Entry为null停止 for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) { //循环中的Entry中的key ThreadLocal<?> k = e.get(); if (k == key) { //如果key相同则替换value e.value = value; //将Entry中ThreadLocal为null的赋值给当前槽中 tab[i] = tab[staleSlot]; //在将Entry赋值给原来ThreadLocal为null的槽中。 //这两行操作相当于把槽里的内容互换了,达到的效果是前边的槽中的Entry有key,循环中的也就是后边的没有key tab[staleSlot] = e; //如果列表向左查询没有发现Entry中key有null的。则将当前循环中的槽的位置赋值。 //因为上两步操作已经把当前槽变成了key为null的槽,所以此处记录的位置就是key是null的位置 //如果向左查询有Entry里是null值那就表示这个区间内还有更左边有key是null的 if (slotToExpunge == staleSlot){ slotToExpunge = i; } //清理槽 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } //当前循环中的槽也是被GC过的。并且向左查询没有发现Entry为null的,就记录当前槽的位置。 if (k == null && slotToExpunge == staleSlot){ slotToExpunge = i; } } //出循环只有一种情况,key为null的Entry下标往后寻找没有发现与当前ThreadLocal相同的key。 //此时需要将原来Entry的value职位null。此操作用来释放内存。 tab[staleSlot].value = null; //创建一个新的Entry其中key是当前ThreadLocal,value是set的参数。将它放到被GC的位置。 tab[staleSlot] = new Entry(key, value); //如果向左查询有Entry中key是null的slotToExpunge就是在左边确定的 //如果向左查询没有Entry中key是null的,而向右查询有Entry中key是null的slotToExpunge就是右边确定的。 //如果两边都没有的情况表示当前区间内只有staleSlot一个为Entry是null的而这种情况下直接重新覆盖了Entry。不需要清理。条件不成立。 if (slotToExpunge != staleSlot){ cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } } }

    清理槽

    expungeStaleEntry方法就是将废槽清空,然后将哈希冲突的槽重新分配位置,因为哈希冲突后是从哈希位向后移动寻找Entry是null的槽放入的,此后这些冲突的槽可能有被清理的,所以重新分配位置,方法的返回值是Entry为null的位置,cleanSomeSlots方法从这个位置

    继续寻找有没有废槽,如果有就清理。

    static class ThreadLocalMap {
          //接收的参数是槽里没有Entry的槽和当前数组的长度
        private boolean cleanSomeSlots(int i, int n) {
                boolean removed = false;
                Entry[] tab = table;
                int len = tab.length;
                do {
                    //找到下一个槽的位置
                    i = nextIndex(i, len);
                    //获取槽内的Entry
                    Entry e = tab[i];
                    //如果槽内有Entry,并且Entry的key是null,表示这是个废槽。
                    if (e != null && e.get() == null) {
                        n = len;
                        //有废槽肯定要清理的。
                        removed = true;
                        //方法返回下一个槽内没有Entry的槽下标
                        i = expungeStaleEntry(i);
                    }
                } while ( (n >>>= 1) != 0);//这个操作相当于折半除2的操作。10,5,2,0,
                return removed;
        }
        
        //接收的参数是槽下标内有Entry,但是Entry的key被GC了。
        private int expungeStaleEntry(int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
                //将槽清空
                tab[staleSlot].value = null;
                tab[staleSlot] = null;
                //Entry[]--
                size--;
                Entry e;
                int i;
                //循环的开始是废槽的下一个,终止条件是下一个槽有Entry
                for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
                    //拿到槽内的ThreadLocal
                    ThreadLocal<?> k = e.get();
                    //如果槽内的key也是null则表示这也是个废槽,则也需要做清空操作。
                    if (k == null) {
                        e.value = null;
                        tab[i] = null;
                        size--;
                    } else {
                        //如果槽内有的Entry有key,则通过hash值算出槽的位置。
                        int h = k.threadLocalHashCode & (len - 1);
                        //如果算出的槽位置不是当前的位置则表示这个key曾经哈希冲突了,所以位置并不是哈希位。
                        if (h != i) {
                            //将这个槽清空
                            tab[i] = null;
                            //从计算的哈希位开始循环,找到Entry为null的槽,将刚刚清空槽里的Entry重新安置。
                            while (tab[h] != null){
                                h = nextIndex(h, len);
                            }
                            //这一步的操作的意义在于,如果循环中有if条件满足的,这代表当前i这个位置之前有可用的槽,那就从哈希位开始往后找,找到空槽,重新安置这个Entry。
                            tab[h] = e;
                        }
                    }
                }
                return i;//入参staleSlot是一个废槽,返回的i则是一个Entry为null的槽。
        }      
    }

    get方法也会清理槽

    get方法通过当前ThreadLocal获取Entry[]中对应的Entry,如果ThreadLocalMap未实例化则实例化并返回null,通过哈希位找到了就返回,哈希位上的不是当前ThreadLocal则表示哈希冲突,继续在数组后寻找,如果途中发现有废槽则清理,如果最终没有找到则返回null。

    public class ThreadLocal<T> {
           public T get() {
            //获取当前线程
            Thread t = Thread.currentThread();
            //获取线程内的ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            //如果ThreadLocalMap已经实例化
            if (map != null) {
                //通过ThreadLocal这个key到数组中找到Entry,是有可能找不到返回null的
                ThreadLocalMap.Entry e = map.getEntry(this);
                //如果找到了,返回Entry中的value
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            //走到这里两种情况,1 ThreadLocalMap没有实例化,则实例化 2 从Entry[]没有找到对应ThreadLocal的Entry
            return setInitialValue();
        }
        
        //这个方法和set差不多,但是它可以返回null。
        private T setInitialValue() {
            //如果现在使用的就是ThreadLocal则一定返回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;
        }
        //这个方法只能子类重写,意味着可以给ThreadLocal赋默认值。
        protected T initialValue() {
            return null;
        }  
    }
    
    static class ThreadLocalMap {
        static class ThreadLocalMap {
    
        //通过ThreadLocal找Entry
         private Entry getEntry(ThreadLocal<?> key) {
                //计算哈希位
                int i = key.threadLocalHashCode & (table.length - 1);
                //查看哈希位上的Entry
                Entry e = table[i];
                //如果Entry不是null或者Entry的key就是当前的ThreadLocal则找到了返回Entry
                if (e != null && e.get() == key){
                    return e;
                }
                else{
                    //如果从哈希位没有找到Entry或者Entry中的key不是当前ThreadLocal
                    return getEntryAfterMiss(key, i, e);
                }
        }
        //接收的参数是当前ThreadLocal,计算的哈希位,和这个哈希位上的Entry
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
                Entry[] tab = table;
                int len = tab.length;
                //从哈希位上开始循环寻找
                while (e != null) {
                    ThreadLocal<?> k = e.get();
                    //如果找到了key相同的则返回
                    if (k == key){
                        return e;
                    }    
                    //如果当前槽内的key是null则要被清理
                    if (k == null){
                        expungeStaleEntry(i);
                    }else{
                        //如果槽内的key有值则继续寻找。直到Entry位null停止。
                        i = nextIndex(i, len);
                    }
                    //下一个位置继续找
                    e = tab[i];
                }
                //如果循环结束了,表示哈希位往后寻找的key都不是当前的ThreadLocal,返回null。
                return null;
        }    
    }

    扩容

    当数组内的元素到达阈值后触发扩容,扩容操作进行前会遍历数组进行清理。如果清理后仍然达到阈值则二倍扩容,循环扩容前的数组,根据新数组的长度重新计算哈希值,如果哈希槽内没有元素则放入,如果有则线性查询可用槽放入。然后用新的数组替换老的数组。

    static class ThreadLocalMap {
       //扩容
             private void rehash() {
                //清理一遍槽
                expungeStaleEntries();
                //大于阈值扩容
                if (size >= threshold - threshold / 4)
                    resize();
            }
            
            //全部清理
            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);
                    }
                }
            }
            
            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];
                    //如果Entry不是null
                    if (e != null) {
                        //获取key
                        ThreadLocal<?> k = e.get();
                        if (k == null) {
                            //key是null清理
                            e.value = null; 
                        } else {
                            //根据哈希值算出来在新的数组中的位置。
                            int h = k.threadLocalHashCode & (newLen - 1);
                            //新的位置上有Entry表示哈希冲突,则继续向后寻找。
                            while (newTab[h] != null){
                                h = nextIndex(h, newLen);
                            }
                            //找到一个Entry为null的位置存放Entry。
                            newTab[h] = e;
                            count++;
                        }
                    }
                }
                //设置新的阈值
                setThreshold(newLen);
                //新数组内元素的总个数
                size = count;
                //替换数组
                table = newTab;
            }    
    }

    手动清理的重要性

    clear方法就是把ThreadLocal从Entry中删除,然后删除Entry。这样Entry就没有了引用会被GC。如果不使用clear,那么就算是ThreadLocal栈内存释放了,这个对象还是存在于Thread里的ThreadLocalMap里的Entry[]数组中,除非遇到GC否则永远存在。手动清理的作用就在于不用等待GC自己把Entry清理。

    public class ThreadLocal<T> {
      //通过ThreadLocalMap的remove方法释放内存
        public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
        }
        
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }  
    }
    
    static class ThreadLocalMap {
       //通过当前ThreadLocal删除
        private void remove(ThreadLocal<?> key) {
                Entry[] tab = table;
                int len = tab.length;
                //计算哈希位
                int i = key.threadLocalHashCode & (len-1);
                //循环找匹配的key
                for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                    if (e.get() == key) {
                        //调用Refereence的clear把key清空
                        e.clear();
                        //再次清理槽。
                        expungeStaleEntry(i);
                        return;
                    }
                }
        }
    }
    
    public abstract class Reference<T> {
      private T referent;//这个就是ThreadLocal对象
        public void clear() {
            this.referent = null;
        }  
    }

     

  • 相关阅读:
    前沿科技相关
    52ABP
    C#常用及注意点
    电商秒杀系统:电商微服务框架组件
    面向对象OOP
    《CLR via C#》书籍
    .NET发布时选择【独立部署模式】引发的故事
    unity 3D物体使用EventSystem响应事件
    协程
    unity 2d碰撞/ui组件碰撞
  • 原文地址:https://www.cnblogs.com/zumengjie/p/15579144.html
Copyright © 2011-2022 走看看