资讯专栏INFORMATION COLUMN

ThreadLocal源码

objc94 / 2355人阅读

摘要:计算哈希后确定的槽内是没有表示没有哈希冲突,此时一个放入槽内。下一个位置继续找如果循环结束了,表示哈希位往后寻找的都不是当前的,返回。新的位置上有表示哈希冲突,则继续向后寻找。

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 {    //计算出来的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> {            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 {       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 {  //通过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 {  private T referent;//这个就是ThreadLocal对象    public void clear() {        this.referent = null;    }  }

 

 

 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/124534.html

相关文章

  • 深入ThreadLocal原理剖析

    摘要:毫秒是为了体现出是否存在线程安全问题。因为是线程对象的成员变量,所以生命周期等同于线程的生命周期。这个类是私有的,允许在类线程中声明字段。操作是为了控制两个线程的执行流程。总结不可继承,是当前线程的成员变量,在子线程中不可见。 ...

    1treeS 评论0 收藏0
  • 追踪解析 ThreadLocal 源码

    摘要:虽然类名中带有字样,但是实际上并不是接口的子类。是弱连接接口,这意味着如果仅有指向某一类,其任然有可能被回收掉。这里使用弱连接的意义,是为了防止业务代码中置空对象,但是由于存在连接可达,所以仍然无法回收掉该对象的情况发生。 零 前期准备 0 FBI WARNING 文章异常啰嗦且绕弯。 1 版本 JDK 版本 : OpenJDK 11.0.1 IDE : idea 2018.3 2 T...

    wawor4827 评论0 收藏0
  • ThreadLocal源码

    摘要:下来我们来看中的方法。从中可以看到真正保存的是在中,接着看看的源码。数组的初始长度为,最多可保存一旦超过就进行扩容增加一倍。而内部利用数组来保存和值的,数组的索引就是的哈希值数组的长度。 在Android-27中查看源码: 在Looper源码中,我们看到通过ThreadLocal的set方法来保存Looper,通过get方法来取出Looper。下来我们来看ThreadLocal中的se...

    mzlogin 评论0 收藏0
  • ThreadLocal源码

    摘要:下来我们来看中的方法。从中可以看到真正保存的是在中,接着看看的源码。数组的初始长度为,最多可保存一旦超过就进行扩容增加一倍。而内部利用数组来保存和值的,数组的索引就是的哈希值数组的长度。 在Android-27中查看源码: 在Looper源码中,我们看到通过ThreadLocal的set方法来保存Looper,通过get方法来取出Looper。下来我们来看ThreadLocal中的se...

    岳光 评论0 收藏0
  • 深入理解Python中的ThreadLocal变量(中)

    摘要:在深入理解中的变量上中我们看到的引入,使得可以很方便地在多线程环境中使用局部变量。特别需要注意的是,基类的并不会屏蔽派生类中的创建。到此,整个源码核心部分已经理解的差不多了,只剩下用来执行清除工作。 在 深入理解Python中的ThreadLocal变量(上) 中我们看到 ThreadLocal 的引入,使得可以很方便地在多线程环境中使用局部变量。如此美妙的功能到底是怎样实现的?如果你...

    DataPipeline 评论0 收藏0
  • 您有一份ThreadLocal完全解析手册

    摘要:返回索引位置的值。因为依赖于静态成员变量的关系,所以它的肯定唯一获取当前线程。位置还没有初始化第一次这个,直接将放到的位置。在线程池模式下,生命周期伴随着线程一直存在,可能出现内存泄漏的情况,最好手动调用方法。 本文原创地址,:jsbintask的博客(食用效果最佳),转载请注明出处! 前言 ThreadLocal是jdk中一个非常重要的工具,它可以控制堆内存中的对象只能被指定线程访问,如...

    刘东 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<