ThreadLocal原理解析(二)之---ThreadLocalMap

在了解ThreadLocalMap之前,先了解下Java的弱引用;
弱引用的定义:
弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存.通过使用弱引用来避免内存泄漏.

Product productA = new Product();
WeakReference<Product> weakProductA = new WeakReference<>(productA);

在这里插入图片描述

接下来主要看ThreadLocal中的ThreadLocalMap静态方法
ThreadLocalMap中:通过key 使用弱引用,key保存的是ThreadLocal对象;
value: 使用的是强引用,value保存的是ThreadLocal对象与当前线程相关联的value
这样设计的好处是,当ThreadLocal对象失去强引用后且对象GC回收后,散列表中与threadLocal对象关联的entry#key再次去 key.get()时,拿到的是null; 这样可以区分哪些entry是过期的。

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

接下来有几个常规属性
INITIAL_CAPACITY:初始化当前threadLocal散列表的 初始长度

  private static final int INITIAL_CAPACITY = 16;

table:threadLocalMap 内部散列表数组引用, 数组的长度必须是2的次方数

     private Entry[] table;

size:当前散列表数组 占用情况,存放多个entry

 private int size = 0;

threshold: 扩容出发阈值 ,当初始值为: len *2/3

private int threshold; // Default to 0

nextIndex()方法: 参数1: 当前下标; 参数2: 当前散列表数组长度

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

prevIndex()方法:

        private static int prevIndex(int i, int len) {
            // 当前下标-1 大于等于0 返回 -1后的值就OK
            // 否则说明当前下标 -1 == -1 此时 当前返回散列表最大下标。
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

ThreadLocalMap的构造方法:因为Thread.threadLocals字段是延迟初始化的,只有线程第一次存储 threadLocal-value时,才会创建threadLocalMap对象.有两个参数;firstKey:threadLocal对象; firstValue: 当前线程与threadLocal对象关联的value;

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 创建一个为INITIAL_CAPACITY ,表示threadLocalMap内部的散列表
            table = new Entry[INITIAL_CAPACITY];
            // 寻址算法: key.threadLocalHashCode & (table.length -1 )
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 创建一个entry
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            // 设置当前的扩容阈值
            setThreshold(INITIAL_CAPACITY);
        }

getEntry()方法:

 private Entry getEntry(ThreadLocal<?> key) {
            // 获取当前ThreadLocal的hasCode,ThreadLocal.threadLocalHashCode &(table - 1)
            int i = key.threadLocalHashCode & (table.length - 1);
            // 访问散列表中 指定位置的slot
            // 说明entry #key 与当前查询的key 一致,直接返回上层就行
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                // 继续向当前桶位后面继续搜索key ==  e.key 的entry
            // 为什么这样做?
            // 因为 存储时, 发生hash冲突后, 并没有在entry层面形成 链表
            // 存储时的处理 就是线性的向后找到一个可以使用的slot,并且存放进去
                return getEntryAfterMiss(key, i, e);
        }

set()方法:

 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;
            // 计算ThreadLocal对象在当前散列表中 对应的位置
            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();
                // 说明key 一致,是一个替换操作的逻辑
                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;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

replaceStaleEntry()方法

 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).
            // 不碍事就是谈测式 清理过期数据的 开始下标. 默认从当前 staleSlot开始
            int slotToExpunge = staleSlot;

            // 以当前staleSlot 开始,向前迭代查找, ,找没有过期的数据。一直到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
            // 以当前staleSlot向后去查找,直到碰到 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.
                // 如果key 与插入的key一致,说明找到了替换逻辑
                if (k == key) {
                    // 替换新旧数据
                    e.value = value;

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

                    // Start expunge at preceding stale entry if it exists
                    // 如果成立: 说明方法一开始向前查找过期数据 并未找到 过期的entry
                    if (slotToExpunge == staleSlot)
                        // 开始探测清理过期数据的下标,修改为当前循环的 index
                        slotToExpunge = i;
                    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.
                // 条件一: k== null 说明:当前遍历的entry是一个过期数据
                // 条件二: 说明方法一开始向前查找过期数据 并未找到 过期的entry
                if (k == null && slotToExpunge == staleSlot)
                    // 因为向后查询过程中,查找到了一个过期数据,进行更新slotToExpunge 为当前位置
                    // 前提条件 是 前驱扫描 时 未发现过期数据
                    slotToExpunge = i;
            }

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

            //直接将新数据添加到 table[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);
        }

在这里插入图片描述

expungeStaleEntry()方法

private int expungeStaleEntry(int staleSlot) {
            // 获取散列表
            Entry[] tab = table;
            // 获取散列表当前长度
            int len = tab.length;

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

            // Rehash until we encounter null
            // 表示当前遍历的entry
            Entry e;
            // 表示当前遍历的下标
            int i;
            // for循环 从 staleSlot +1 的位置开始 搜索过期数据,直到碰到 slot == null 结束
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                // 进入到

                // 获取当前遍历节点 entry的 key
                ThreadLocal<?> k = e.get();
                // 条件成立: 说明k 表示的 threadLocal对象 已经被GC回收了.......当前entry 属于脏数据了
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    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;
        }

在这里插入图片描述

cleanSomeSlots()方法:

 private boolean cleanSomeSlots(int i, int n) {
            // 表示启发式清理工作 是否清除过期数据
            boolean removed = false;
            // 获取当前map的散列表引用
            Entry[] tab = table;
            // 获取当前散列表的长度
            int len = tab.length;

            do {
                // 获取当前i的下一个下标
                i = nextIndex(i, len);
                // 获取table 中当前下标为i的元素
                Entry e = tab[i];
                // 条件1: e != null
                //条件二:  e.get() == null; 说明当前slot中保存的entry 是一个过期的数据
                if (e != null && e.get() == null) {
                    // 重新更新n 为 table数组长度
                    n = len;
                    // 表示清理过数据
                    removed = true;
                    // 以当前过期的slot 为开始节点
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

virtuousOne

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值