ThreadLocal源码第二讲(ThreadLocalMap)


1、ThreadLocalMap 简介

什么是弱引用?

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

a是强引用,因为是强引用,后面那个new的A对象不会被GC 回收,但当a==null时,new的对象不会因为有weakA 而保留,依然会被GC回收,因为weakA 是一个弱引用
在这里插入图片描述

2、源码分析

下面详细讲解一下 ThreadLocalMap 的源码

成员属性

ThreadLocalMap 内部类的属性

      /** 什么是弱引用?
         * A a = new A();  强引用
         * WeakReference weakA = new WeakReference(a); 弱引用
         *
         * a = null
         * 下次GC时 对象 a 就被回收了,
         *
         * key使用的是弱引用保留,key保留的是ThreadLocal对象
         * value 使用的是强引用,value保存的是ThreadLocal对象与当前线程相关联的value
         *
         * entry的 key使用弱引用有什么好处?
         * 当ThreadLocal对象失去强引用且对象GC 回收后,散列表中的 与ThreadLocal对象相关联的entry#key再次去key.get() 时,拿到的是null
         * 站在map角度就可以区分出哪些entry是过期的,哪些entry是非过期的。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

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

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         * ThreadLocalMap 内部散列表数组引用,数组的长度 必须是2的次方
         */
        private Entry[] table;

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

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

一些小方法

分别是setThreshold ,nextIndex ,prevIndex ,见名知意
这些方法并不是主要的方法,而是主要方法中调用的一些小的操作方法,比较简单,所以就放在一起看。

     /**
         * 将阈值设置为当前数组长度的 2/3;
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * 参数1:当前下标   参数2:当前散列表数组长度
         */
        private static int nextIndex(int i, int len) {
            //当前下标+1 小于 散列表数组的话,返回len+1
            //否则 下标+1 == 散列表数组的长度,返回0;
            //实际形成一个环绕式的访问
            return ((i + 1 < len) ? i + 1 : 0);
        }

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

构造方法

实际上有两个构造方法,另一个是私有构造方法:private ThreadLocalMap(ThreadLocalMap parentMap)

       /**
         * 构造方法:
         * 因为 Thread.threadLocals 字段时延迟初始化的,只有线程第一次存储Entry(threadLocal-value) 时,才会创建threadLocalMap对象
         *
         * firstKey:threadLocal对象
         * firstValue:当前线程与threadLocal对象关联的value
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //创建entry 数组长度为16,表示threadLocalMap内部的散列表
            table = new Entry[INITIAL_CAPACITY];
            //寻址算法:key.threadLocalHashCode & (table.length - 1)
            //table.length 数组的长度一定是2的次方
            //table.length-1 有什么特征? 转化为2进制后都是1:  16 - 1 =》 1 0000 - 1 =》 1111
            //1111 与任何数组进行 & 运算后 得到的数值 一定是 小于等于 1111

            //i 计算出来的结果 一定是 小于等于 1111 的
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //创建entry对象 存放到指定位置的slot中
            table[i] = new Entry(firstKey, firstValue);
            //设置size=1
            size = 1;
            //设置扩容阈值 16 * 2/3 =10
            setThreshold(INITIAL_CAPACITY);
        }

getEntry方法

这个方法是 ThreadLocal.get()的主要实现方法,get的逻辑就是来源这个方法

    /**
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         *
         * ThreadLocal对象 get() 操作实际上是有ThreadLocalMap.getEntry()代理完成的
         * key:某个ThreadLocal对象,因为散列表中存储的entry.key 类型四 ThreadLocal
         */
        private Entry getEntry(ThreadLocal<?> key) {
            //路由规则:ThreadLocal.threadLocalHashCode & (table.length - 1)=> index
            int i = key.threadLocalHashCode & (table.length - 1);
            //访问散列表中指定位置的slot
            Entry e = table[i];
            //条件一:成立,说明slot有值
            //条件二:成立,说明entry&key 与当前查询的key一致,返回当前entry 给上层就可以了
            //key是弱引用
            if (e != null && e.get() == key)
                return e;
            else
                //有几种情况到这里?
                //1. e==null
                //2.e.key != key

            //getEntryAfterMiss 方法 会继续向当前桶位后面继续搜索 e.key == key 的entry
            //因为存储时 发生hash冲突,并没有在entry层面形成链表,存储时的处理 就是线性的向后找到一个可以使用的slot,并存放进去
                return getEntryAfterMiss(key, i, e);
        }

getEntryAfterMiss方法

       /**
         * @param  key the thread local object             ThreadLocal对象 就是 key
         * @param  i the table index for key's hash code    key算出来的index
         * @param  e the entry at table[i]                  table[index] 中的Entry
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            //获取当前ThreadLocalMap 中的散列表table
            Entry[] tab = table;
            //获取table长度
            int len = tab.length;

            //条件:e!=null 说明向后查找的范围是有限的,碰到slot==null 的情况,搜索结束
            //e:循环处理的当前元素
            while (e != null) {
                //获取当前slot中entry对象的key
                ThreadLocal<?> k = e.get();
                //条件成立:说明向后查询过程中找到合适的entry了,返回entry就ok了
                if (k == key)
                    return e;

                //条件成立:说明当前slot中的entry#key 关联的 ThreadLocal对象已经被GC回收了,因为key是弱引用,e.get() == null
                if (k == null)
                    //做一次探测式过期数据回收
                    expungeStaleEntry(i);
                else
                    //更新index,继续向后搜索
                    i = nextIndex(i, len);
                //获取下一个slot中的entry
                e = tab[i];
            }
            //执行到这里说明:关联区段内 都没有找到相关数据
            return null;
        }

expungeStaleEntry方法

探测式清理过期数据:向后查找过期数据,碰到slot==null就返回它的下标

       /**
         * A a = new A()
         * WeakReference b = new WeakReference(a)
         * a=null
         * 此时 b也等于null
         *
         * 参数stateSlot  table[stateSlot] 就是一个过期数据,以这个位置开始 继续向后查找过期数据,知道碰到slot==null的情况结束
         */
        private int expungeStaleEntry(int staleSlot) {
            //获取散列表
            Entry[] tab = table;
            //获取散列表当前长度
            int len = tab.length;

            // expunge entry at staleSlot
            //help GC
            tab[staleSlot].value = null;
            //因为staleSlot位置的entry 是过去的 这里直接置为null
            tab[staleSlot] = null;
            //因为上面干掉一个元素,所以 -1;
            size--;

            // Rehash until we encounter null
            //e:表示当前遍历节点
            Entry e;
            //i:表示当前遍历的index
            int i;

            //for 循环从staleSlot+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;
                    //因为上面干掉一个元素,所以 -1;
                    size--;
                } else {
                    //执行到这里,说明当前遍历的slot中对应的entry是非过期数据
                    //因为前面有可能清理掉了几个过期数据
                    //且当前entry存储时有可能碰到hash冲突,往后偏移存储了,这个时候 应该去优化位置,让这个位置更可靠近 正确位置
                    //这样的话,查询的时候 效率才会更高


                    //重新计算当前entry 对应的index
                    int h = k.threadLocalHashCode & (len - 1);

                    //条件成立:说明当前entry存储时 就是发生hash冲突,然后向后偏移过了
                    if (h != i) {
                        //将entry 当前位置 设置为 null
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.

                        //h 是正确位置

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

在这里插入图片描述

set方法

ThreadLocal.set() 方法的 主要实现就是通过ThreadLocalMap.set()来实现的。

     /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         *
         * ThreadLocal 使用set方法 给当前线程添加 ThreadLocal-value 键值对
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            //获取散列表
            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)]) {
                //获取当前元素key
                ThreadLocal<?> k = e.get();

                //条件成立:说明当前set操作时替换操作
                if (k == key) {
                    e.value = value;
                    return;
                }
                //条件成立:说明向下寻找过程中 碰到entry#key == null 的情况了,说明当前entry 是过期数据
                if (k == null) {
                    //2. 碰到一个过期的 slot,这个时候就可以强行占用
                    //替换逻辑
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //执行到这里,说明for循环碰到了 slot==null的情况

            //在合适的slot中创建新的entry元素
            tab[i] = new Entry(key, value);
            //因为是新添加,size++
            int sz = ++size;

            //做一次启发式清理
            //条件一:!cleanSomeSlots(i, sz)成立:说明启发式清理工作,未清理到任何数据
            //条件二:sz >= threshold 成立:说明当前table内的entry已经达到了扩容阈值了,会触发rehash()
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

replaceStaleEntry方法

在set()方法向下寻找可用桶位的过程中,如果碰到Entry.key == null (弱引用) 的情况,说明当前entry是过期数据,这个时候可以强行占用该桶位,通过replaceStaleEntry方法执行替换过期数据的逻辑:

       /**
         * @param  key the key
         * @param  value the value to be associated with key
         * @param  staleSlot index of the first stale entry encountered while
         *         searching for key.
         *
         *  替换过期的slot
         *
         *   key:键 ThreadLocal对象
         *   value:val
         *   staleSlot:上层方法 set方法,迭代查找时,发现的当前这个slot是一个过期的entry
         */
        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).

            //表示 开始探测式清理过期数据的 开始下标,默认从当前stateSlot开始
            int slotToExpunge = staleSlot;

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

            // Find either the key or trailing null slot of run, whichever
            // occurs first

            //以当前 stateSlot 向后去查找,知道碰到null为止
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                //获取当前元素key
                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;

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

                    // Start expunge at preceding stale entry if it exists
                    //条件成立:
                    //  1.说明 replaceStaleEntry一开始时的向前查找过期数据 并未找到过期的entry
                    //  2.向后检查过程中也未发现过期数据(向后查询是下面那个if做个事情)
                    if (slotToExpunge == staleSlot)
                        //开始探测式清理过期 数据的下标 修改为 当前循环的index
                        slotToExpunge = i;

                    //cleanSomeSlots:启发式清理
                    //参数: 探测式 结束的位置(entry==null的位置)数组长度
                    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.
                //条件1: k==null 成立:说明当前遍历的entry 是一个过期数据..
                //条件2:slotToExpunge == staleSlot成立:一开始时的向前查找过期数据 并未找到过期的entry
                if (k == null && slotToExpunge == staleSlot)
                    //因为向后查询过程中查找到一个过期数据了,更新slotToExpunge 为当前位置
                    //前提条件:前驱扫描时 未发现过期数据
                    slotToExpunge = i;
            }

            //什么时候执行到这里?
            //向后查找过程中 并未发现 k==key 的entry,说明当前set操作 是一个添加逻辑

            // If key not found, put new entry in stale slot
            //直接将新数据添加到 tab[staleSlot] 对应的slot中
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            //条件成立:除了当前staleSlot以外,还未发现其它的过期slot了,所以要开启清理数据的逻辑
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

在这里插入图片描述

cleanSomeSlots方法

启发式清理过期节点:
启发式清理工作的开始位置就是 :探测式 结束的位置(entry==null的位置)

      /**
         * 参数1:启发式清理工作的开始位置,探测式 结束的位置(entry==null的位置)
         * 参数2:一般是数组长度table.length,这里n表示结束条件
         */
        private boolean cleanSomeSlots(int i, int n) {
            //表示启发式清理工作 是否清理过过期数据
            boolean removed = false;
            Entry[] tab = table;
            //散列表长度
            int len = tab.length;

            do {

                //为什么这里是从i+1 开始呢?
                //cleanSomeSlots(i = expungeStaleEntry(slotToExpunge), len);
                //因为 i = expungeStaleEntry(slotToExpunge) 返回值一定是null的下标

                //获取当前i 的下一个下标
                i = nextIndex(i, len);
                Entry e = tab[i];
                //条件一:e != null成立
                //条件二:e.get() == null 成立,说明当前slot中保存的entry 是一个过期的数据
                if (e != null && e.get() == null) {
                    //重新更新n 为table数组长度
                    n = len;
                    //表示清理过数据
                    removed = true;
                    //以当前过期的slot 为开始节点 做一次 探测式清理工作
                    i = expungeStaleEntry(i);
                }
                //探测5次
                //假设table 长度为16
                //16 >>> 1 ==> 8
                //8 >>> 1 ==> 4
                //4 >>> 1 ==> 2
                //2 >>> 1 ==> 1
                //1 >>> 1 ==> 0
            } while ( (n >>>= 1) != 0);
            return removed;
        }

在这里插入图片描述

rehash方法

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

            // Use lower threshold for doubling to avoid hysteresis
            //条件成立:说明清理完过期数据后,当前散列表内的entry数量 任然达到了 阈值的3/4,真正触发扩容
            if (size >= threshold - threshold / 4)
                //真正扩容
                resize();
        }
}

resize方法

真正扩容的方法:

   private void resize() {
            //获取当前散列表
            Entry[] oldTab = table;
            //获取当前散列表长度
            int oldLen = oldTab.length;
            //计算新表长度
            int newLen = oldLen * 2;
            //创建一个新的table散列表
            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#key
                    ThreadLocal<?> k = e.get();
                    //条件成立:说明老表中的当前位置的entry是一个过期数据,help GC,不用 在迁移
                    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;
            //将扩容后的新表的引用保存到 threadLocalMap 对象的table这里
            table = newTab;
        }

remove方法

        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)]) {
                if (e.get() == key) {
                    e.clear();
                    //探测式清理
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值