ThreadLocal源码分析_02内核(ThreadLocalMap)

ThreadLocal源码分析_02内核(ThreadLocalMap)

本篇主要介绍ThreadLocal的内核ThreadLocalMap(ThreadLocal的静态内部类),在学习ThreadLocalMap内核之前,再来复习一下ThreadLocal的执行流程:

heJ5uj.png

下面就正式分析一下ThreadLocalMap的原理:

1、成员属性

        /**
         * 初始化当前map内部散列表数组的初始长度
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * ThreadlocalMap内部散列表数组的引用,数组的长度必须是2的次方数
         */
        private Entry[] table;

        /**
         * 当前散列表数组占用情况,存放多少个entry
         */
        private int size = 0;

        /**
         * 扩容出发阙值,初始值为:len*2/3
         * 触发后调用reshash()方法
         * rehash()方法先做一次全量检查全局过期数据,把散列表中所有过期的entry全部移除
         * 如果移除之后,当前散列表中的entry个数仍然达到(threshold-threshold/4)
         * 即当前threshold阙值的3/4就进行扩容
         */
        private int threshold; // Default to 0

        /**
         * 将扩容阙值设置为散列表当前数组长度的2倍/3
         * @param len
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

2、内部类

这里内部类Entry继承了弱引用WeakReference:

  • 什么是弱引用呢?eg:

    A a = new A();     								// 强引用
    WeakReference weakA = new WeakReference(a);  	// 弱引用
    

    当a=null的时候,则下一次GC的时候对象a就倍回收了,不管有没有弱引用在关联这个对象:

    hn01k4.png

  • key使用的是弱引用保留,key保存的是threadLocal对象。

  • value使用的是强引用,value保存的是threadLocal对象与当前线程相关联的value

		/**
         * ThreadLocalMap的Entry内部类,成员属性有两个,key(threadLocal对象)
         * 和value(threadlocal对象与当前线程相关联的value)
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            //value:使用的是强引用,value保存的是threadLocal对象与当前线程相关联的value
            Object value;

            //k:key 使用的是弱引用,key保存的是threadLocal对象
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

Entry的key这样设计有什么好处呢?

  • 当threadLocal对象失去强引用且对象倍GC回收之后,散列表中的与threadLocal对象相关联的entry#key再次失去,key.get()时,拿到的是null
  • 站在map的角度就可以区分出entry哪些是过期的,哪些是entry是非过期的

3、构造方法

  • Thread.threadLocals字段是延迟初始化的,只有线程第一次存储threadLocal-value的时候才会创建。
		/**
         *
         * @param firstKey threadLocal对象
         * @param firstValue 当前线程与ThreadLocal相关联的value
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //创建entry数组长度为18,表示threadLocalMap内部的散列表
            table = new Entry[INITIAL_CAPACITY];
            //寻址算法:key.threadLocalHashCode & (table.length-1),确定要存储的下标值
            //table的长度一定是2的次方数
            //2的次方数-1有什么特征呢?转化为2进制后都是1 16 10000 => 1111
            //1111在与任何数值进行&运算后,得到的数值一定是<=1111
            //i计算出来的结果一定是<=1111
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //创建entry对象存放到指定位置的slot中
            table[i] = new Entry(firstKey, firstValue);
            //设置当前数组中的元素数量为1
            size = 1;
            //设置扩容阙值 (当前数组的长度*2/3)
            setThreshold(INITIAL_CAPACITY);
        }
        
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

4、成员方法

nextIndex方法

		/**
         * 获得下一个数组下标的位置
         * @param i 当前散列表数组下标
         * @param len 当前散列表数组长度
         * @return
         */
        private static int nextIndex(int i, int len) {
            //当前数组下标i+1,如果计算结果:小于散列表的长度,则返回+1后的值
            //否则:这种情况就是下标i+1=len 返回0
            return ((i + 1 < len) ? i + 1 : 0);
            //实际上形成一个环绕式的访问,如下图:
        }

hns5q0.png

**prevIndex方法 **

		/**
         * 获得上一个数组下标的位置
         * @param i 当前数组下标
         * @param len 当前散列表数组的长度
         * @return
         */
        private static int prevIndex(int i, int len) {
            //当前下标i-1大于等于0 则返回i-1后的值
            //否则,说明当前下标i-1== -1,此时返回散列表的最大下标
            //实际上形成一个环绕式的访问
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

getEntry()方法

  • ThreadLocal对象的get()方法实际上是由ThreadLocalMap.getEntry()代理完成的。
		/**
         * 根据Threadlocal对象,获取对应的Entry对象
         * key:某个threadLocal对象,因为散列表中存储的entry.key类型是ThreadLocal
         * @param key
         * @return Entry
         */
        private Entry getEntry(ThreadLocal<?> key) {
            //桶位路由规则:ThreadLocal.threadLocalHashCode & (table.length - 1) ==> index
            int i = key.threadLocalHashCode & (table.length - 1);
            //访问散列表中指定位置的solt桶,拿到Entry
            Entry e = table[i];
            //条件1:成立 说明slot有值
            //条件2:成立 说明entry#key 与当前查询的key一致,返回当前entry交给上层就可以了
            if (e != null && e.get() == key)
                return e;
            else
                //有几种情况会执行到这里?
                //情况1:桶位上的(Entry)e==null
                //情况2:根据key没有查询到  说明e.key!=key

                //getEntryAfterMiss方法:会继续向当前桶位向后继续搜索,e.key=key的entry

                //为什么这样做呢?
                //因为存储的时候如果发生hash冲突后,并没有在entry面形成链表的结构
                //那么,当存储的时候,如果hash冲突了,处理的方式就是线性的向后找到一个可以用的slot
                //并将entry对象存放进去
                return getEntryAfterMiss(key, i, e);
        }

getEntryAfterMiss方法

  • 继续在桶位之后的桶中寻找e.key==key条件的桶!
		/**
         * 继续在桶位i之后寻找满足条件e.key==key的桶位
         * @param key threadLocal对象表示 key
         * @param i 表示计算出来的桶位
         * @param e 表示table[i]中的entry 用以做返回值的
         * @return
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            //获取当前threadLocalMap中的散列表对象table
            Entry[] tab = table;
            //获取table的长度
            int len = tab.length;
            //while循环条件:e!=null 说明向后查找的范围是有限的 碰到slot==null的情况,则搜索结束
            //e:循环处理的当前元素
            while (e != null) {
                //获取当前slot中entry对象的key
                ThreadLocal<?> k = e.get();
                //k==key条件成立:说明向后查询的过程中找到合适的entry了,直接返回entry就ok了
                if (k == key)
                    //找到的情况下,就会这里返回entry对象了
                    return e;
                //k==null条件成立:说明当前slot中的entry#key 关联的ThreadLocal对象已经倍GC回收了
                //因为key是弱引用 key=e.get()==null
                if (k == null)
                    //做一次探测式过期数据回收
                    expungeStaleEntry(i);
                else
                    //更新index,继续向后搜索
                    i = nextIndex(i, len);
                //获取下一个slot中的entry
                e = tab[i];
            }
            //执行到这里,说明关联区段内都没找到相应数据
            return null;
        }

expungeStaleEntry方法

  • 探测式清理过期数据:以stateSlot位置开始向后查找过期数据,直到碰到slot==null的情况结束,并返回结束时数组下标位置。
		/**
         *
         * @param staleSlot table[staleSlot]就是一个过期数据
         * 以这个位置开始继续向后查找过期数据,直到碰到slot==null的情况结束
         * @return
         */
        private int expungeStaleEntry(int staleSlot) {
            //获取当前散列表对象
            Entry[] tab = table;
            //获取当前散列表的长度
            int len = tab.length;
            //先将当前的slot过期数据清空
            tab[staleSlot].value = null;
            //因为当前stateSlot位置的entry是过期的,所以直接置为null
            tab[staleSlot] = null;
            //因为上面干掉了一个元素,所以size--
            size--;

            //e:表示当前遍历节点
            Entry e;
            //i:表示当前遍历的index
            int i;
            //for循环从stateSlot+1的位置开始搜索过期数据,直到碰到slot==null为止
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                //进入到for循环里面,当前entry不一定为null
                //获取当前遍历节点 entry的key
                ThreadLocal<?> k = e.get();
                //条件成立:说明 k表示的threadLocal对象已经被GC回收了
                //则,当前的entry属于脏数据了
                if (k == null) {
                    //help gc
                    e.value = null;
                    //脏数据对应的slot置为null
                    tab[i] = null;
                    //因为上面干掉了一个元素,所以size--
                    size--;
                } else {
                    //执行到这里,说明当前遍历的slot中对应的slot中对应的entry是非过期数据
                    //因为前面有可能清理掉了几个过期数据,且当前entry存储时有可能hash冲突了
                    //应该向后偏移,这个时候应该去优化位置,让这个位置更加靠近正确位置!

                    //重新计算当前entry对应的index
                    int h = k.threadLocalHashCode & (len - 1);
                    //条件成立,说明当前entry存储的时候,就是发生过hash冲突,然后向后偏移了
                    if (h != i) {
                        //将当前entry设置为null
                        tab[i] = null;

                        //这时 h是正确位置
                        //以正确位置h开始,开始向后查找第一个可以存放entry的位置
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        //将当前元素放入到距离正确位置更近的位置(有可能就是正确位置)
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

该方法的执行流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w8gFqx6M-1633264897020)(https://z3.ax1x.com/2021/08/26/hutWZQ.png)]

set方法 :

  • ThreadLocal使用set方法给当前线程添加ThreadLocal-value键值对:
		/**
         * ThreadLocal使用set方法给当前线程添加threadLocal-value值
         * @param key threadlocal对象(Entry的key值
         * @param value Entry的value值
         */
        private void set(ThreadLocal<?> key, Object value) {
            //获取散列表
            Entry[] tab = table;
            //获取散列表的长度
            int len = tab.length;
            //计算当前key 在散列表中的对应位置
            int i = key.threadLocalHashCode & (len-1);

            //以当前key对应的slot向后查询,直到找到可以用的slot桶位
            //什么时候slot桶位可以使用呢?
            //1.k==key 说明是替换
            //2.碰到一个过期的slot,这个时候可以强行占用
            //3.查找过程中碰到slot==null
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                //获取当前entry的ThreadLocal对象
                ThreadLocal<?> k = e.get();

                //条件成立:说明当前的set操作是一个替换操作
                if (k == key) {
                    //做替换逻辑
                    e.value = value;
                    return;
                }

                //条件成立:说明向下寻找的过程中碰到entry#key==null的情况了,说明当前entry是过期数据
                if (k == null) {
                    //碰到一个过期的slot,可以强行占用该桶位
                    //替换过期的数据
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            //执行到这里,说明for循环碰到了slot==null的情况
            //在合适的slot中,创建新的entry对象
            tab[i] = new Entry(key, value);
            //因为是新添加,所以++size
            int sz = ++size;
            //做一次启发式的清理:
            //条件1:!cleanSomeSlots(i, sz)成立:说明启发式清理工作未清理到任何数据
            //条件2:sz >= threshold成立 说明当前table内的entry已经达到了扩容阙值..会触发rehash操作
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

replaceStaleEntry方法

  • set()方法向下寻找可用slot桶位的过程中,如果碰到key==null的情况,说明当前entry是过期数据,这个时候可以强行占用该桶位,通过replaceStaleEntry方法执行替换过期数据的逻辑。
		/**
         * 强行占用该桶位以清理过期数据
         * @param key threadLocal对象
         * @param value 当前与threadLocal所关联的变量值
         * @param staleSlot 上层set方法,迭代的时候发现该下标位置的slot是一个过期的entry
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            //获取散列表
            Entry[] tab = table;
            //获取散列表数组的长度
            int len = tab.length;
            //临时变量
            Entry e;

            //slotToExpunge表示开始探测式清理过期数据的开始下标,默认从当前的staleSlot开始
            int slotToExpunge = staleSlot;
            //以当前staleSlot开始,向前迭代查找,找有没有过期的数据,for循环一直到碰到null结束
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                //条件成立:说明向前找到了过期数据,更新探测清理过期数据的开始下标为i
                if (e.get() == null)
                    slotToExpunge = i;

            //以当前staleSlot的后一个位置向后去查找,直到碰到null为止
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                //获取entry元素的threadLocal对象
                ThreadLocal<?> k = e.get();
                //条件成立:说明是一个替换逻辑
                if (k == key) {
                    e.value = value;

                    //交换位置的逻辑...
                    //将table[staleSlot]这个过期数据放到当前循环的table[i]这个位置
                    tab[i] = tab[staleSlot];
                    //将table[staleSlot]中保存为当前entry。这样的话,这个数据位置就被优化了
                    tab[staleSlot] = e;

                    //如果条件成立:
                    //1.说明replaceStaleEntry一开始的时候向前查找过期数据 并未找到过期的entry
                    //2.向后检查的过程中也未发现过期数据
                    if (slotToExpunge == staleSlot)
                        //开始探测式清理过期数据的下标 修改为当前循环的index
                        slotToExpunge = i;
                    //cleanSomeSlots:启发式清理
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                //条件1:k==null成立 说明当前遍历的entry是一个过期元素
                //条件2:slotToExpunge == staleSlot成立,一开始向前查找过期数据并未找到过期的entry
                if (k == null && slotToExpunge == staleSlot)
                    //因为向后查询的过程中查找到一个过期数据,更新slotToExpunge为当前位置
                    //前提条件是前驱扫描的时候未发现过期数据
                    slotToExpunge = i;
            }

            //什么时候执行到这里呢?
            //向后查找的过程中并未发现k==key的entry 说明当前set操作是一个添加逻辑
            //直接将新数据添加到table[staleSlot]
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            //条件成立:除了当前的staleSlot以外,还发现其他过期的slot了,所以要开始清理过期数据
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Pth7xGt-1633264897021)(https://z3.ax1x.com/2021/08/27/hM0uZV.png)]

**cleanSomeSlots方法 **

  • 启发式清理工作的方法:
		/**
         * 启发式清理工作的方法:
         * @param i 启发式清理工作开始的位置
         * @param n 一般传递的是table的长度 这里n也表示结束条件
         * @return
         */
        private boolean cleanSomeSlots(int i, int n) {
            //表示启发式清理工作,是否清除过过期数据
            boolean removed = false;
            //获取当前map的散列表引用
            Entry[] tab = table;
            //获取当前散列表数组的长度
            int len = tab.length;
            do {
                //这里为什么不是从i就检查呢?
                //因为cleanSomeSlots(i=expungeStaleEntry(??),n)
                //expungeStaleEntry(??)这个位置的值一定是null 所以从i+1开始
                i = nextIndex(i, len);
                //获取table中当前下标为i的元素
                Entry e = tab[i];
                //条件1:e!=null 成立说明该slot不为null
                //条件2:e.get()==null成立 说明该entry保存的entry是一个过期数据
                if (e != null && e.get() == null) {
                    //重新更新n为table数组长度
                    n = len;
                    //表示清理过数据
                    removed = true;
                    //探测式清理以当前的slot为开始节点,做一次探测式清理工作
                    i = expungeStaleEntry(i);
                }
                //假设table长度为16
                //16 >>> 1  ==> 8
                //8 >>> 1 ==> 4
                //4 >>> 1 ==> 2
                //2 >>> 1 ==> 1
                //1 >>> 1 ==> 0
            } while ( (n >>>= 1) != 0);
            return removed;
        }

cleanSomeSolts启发式清理流程图:

hM0Wo8.png

reshash方法:

  • 扩容方法:
		 /**
         * 扩容方法
         */
        private void rehash() {
            //这个方执行完以后,当前散列表内所有的过期数据,都会被干掉
            expungeStaleEntries();

            //条件成立:说明清理完过期数据后
            //当前散列表内的entry数量仍然达到了threshold*3/4,则真正触发扩容
            if (size >= threshold - threshold / 4)
                //真正执行扩容过的方法
                resize();
        }

resize方法:

  • 真正执行扩容的方法:
		//真正执行扩容的方法
        private void resize() {
            //获取当前散列表
            Entry[] oldTab = table;
            //获取当前散列表的长度
            int oldLen = oldTab.length;
            //计算出扩容后的散列表容量大小,oldLen*2
            int newLen = oldLen * 2;
            //依据新计算出来的散列表容量创建出一个新的散列表
            Entry[] newTab = new Entry[newLen];
            //表示新table(散列表)中的entry数量
            int count = 0;

            //遍历老表,迁移数据到新表
            for (int j = 0; j < oldLen; ++j) {
                //访问老表的指定位置的slot
                Entry e = oldTab[j];
                //条件成立:说明老表中的指定位置有数据(注意 此时散列表中是没有过期数据的)
                if (e != null) {
                    //获取当前entry的ThreadLocal对象(即key)
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        //执行到这里,说明老表的当前位置是非过期元素:即正常数据,需要前移到新表中的数据
                        //计算出当前entry在扩容之后的在新表中的存储位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //while循环就是拿到一个距离h最近的一个可以使用的slot
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        //将数据存放到新表合适的slot中
                        newTab[h] = e;
                        //数量+1
                        count++;
                    }
                }
            }

            //设置下一次触发扩容的指标
            setThreshold(newLen);
            //设置当前散列表中元素的个数
            size = count;
            //将扩容后的新表的引用保存到了ThreadLocal对象的table这里
            table = newTab;
        }

remove方法

  • remove方法就比较简单了
		/**
         * 从threadlocalmap对象的table散列表中移除对应key的entry对象
         * @param key
         */
        private void remove(ThreadLocal<?> key) {
            //获取当前散列表对象
            Entry[] tab = table;
            //获取当前散列表对象的长度
            int len = tab.length;
            //计算出当前Threadlocal在散列表中的下标
            int i = key.threadLocalHashCode & (len-1);
            //由于threadLocalMap对象是采用hash偏移的方式来解决hash冲突的问题,所以循环条件是从
            //当前计算的下标开始直到遇到e==null为止
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    //探测式清除过期数据
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

table;
//获取当前散列表对象的长度
int len = tab.length;
//计算出当前Threadlocal在散列表中的下标
int i = key.threadLocalHashCode & (len-1);
//由于threadLocalMap对象是采用hash偏移的方式来解决hash冲突的问题,所以循环条件是从
//当前计算的下标开始直到遇到e==null为止
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
//探测式清除过期数据
expungeStaleEntry(i);
return;
}
}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值