ThreadLocal源码阅读五:核心方法set源码探究

背景

  1. 推荐阅读ThreadLocal工作过程魔数的学习和疑问思考ThreadLocal解决Hash碰撞

  2. 探究ThreadLocal源码中核心方法set的工作过程及其实现细节。

过程

  • set的入口函数 在这里插入图片描述 注释含义:把当前线程中的本地变量值,设置到具体的容器entry[]中。大多数ThreadLocal的子类不要重写set方法去设置初始值。因为initialValue方法给线程本地变量设置值。

细节

  1. 获取当前线程
  2. 获取当前线程属性ThreadLocalMap
  3. 判断当前线程属性ThreadLocalMap是否为null
  4. 如果不为空,则直接设置值
  5. 如果为空,则创建ThreadLocalMap
  • 创建ThreadLocalMap 在这里插入图片描述

注释含义:创建一个与ThreadLocal关联的map容器。

细节

  1. 调用ThreadLocalMap的有参构建器

    在这里插入图片描述

    注释含义:构造一个新的map.初始值是(firstKey, firstValue)。ThreadLocalMap的构建是懒加载的。当至少有一个entry实例需要放入到map中的时候,我们才创建一个map容器。

    细节

    1. 创建table,也就是给Entry[]赋值。其中,INITIAL_CAPACITY是16
    2. 通过魔数0x61c88647及其长度算得tab中的下脚标值
    3. 给这个table[i]赋值。
    4. 指定大小为1,但是长度是16
    5. 设置扩容阈值(为容量的三分之二大小)

    在这里插入图片描述 注释含义:ThreadLocalMap持有Entry[],而这些Entry又继承了WeakReference。使用引入属性作为key,而这个key总是ThreadLocal对象。当key为null的时候(比如执行entry.get == null)意味着key将永远不会被引用到了,因此entry实例需要从这个table中移除。在后面的代码中,把这样的entry叫做 stale entry。

    细节

    1. 代码执行红框中的构建函数
    2. 先调用父类的构建器
    3. 然后给Object value赋值即可

    在这里插入图片描述

    因此,我们说entry中的key,ThreadLocal是弱引用。


  • 不创建ThreadLocalMap 在这里插入图片描述

    细节

     1. 先忽略for循环中执行的逻辑,new Entry(key, value)上面逻辑已说明。
     2. 执行cleanSomeSlots,表示把entry不为null,而key为null的移除掉。
     3. 判断,如果没有移除且sz大于10(16 * 2 / 3), 则执行rehash()函数。
    复制代码
  • cleanSomeSlots(i, sz)

    1. 此方法的注释

    在这里插入图片描述

注释含义

试探性地扫描数组中的元素,以便找到无效entry实例。当我们创建一个新的entry实例的时候,这个方法就会被调用到。扫描的次数是以2为底数,长度为指数的log函数。如果我们要扫描所有数组的内容的话,我们就需要变量整个数组,这会花费O(N)时间,而我们垃圾收集器是(fast but retains garbage),为了在两者种取到平衡。选择log函数次数扫描。

从名字就可以看出,some,而不是all。

  1. 此方法的源码 在这里插入图片描述 细节

    先不看if中代码段,重新给n赋值为长度的值。

    看循环是如何判断的?

    这个时候,假如i是14,通过 i = nextIndex(i, len),i = 15,tab[15]是null。

    进行while循环判断(n >>>= 1),这个时候n(16)除以2就是8,8 != 0。 继续执行do,这个时候通过 i = nextIndex(i, len)后,i = 0,tab[0]是null。

    进行while循环判断(n >>>= 1),这个时候n除以2就是4,4 != 0 。 继续执行do,这个时候通过 i = nextIndex(i, len),i = 1, tab[1]是null。

    进行while循环判断(n >>>= 1),这个时候n除以2就是2,2 != 0 。 继续执行do,这个时候通过 i = nextIndex(i, len),i = 2, tab[2]是null。

    进行while循环判断(n >>>= 1),这个时候n除以2就是1,1 != 0 。 继续执行do,这个时候通过 i = nextIndex(i, len),i = 3, tab[3]是null。

    进行while循环判断(n >>>= 1),这个时候n除以2就是0,0 != 0 。退出循环

    一旦进入if判断逻辑中。那么n的值又被设置成长度值16了。然后又循环上面的整个逻辑判断过程。

    expungeStaleEntry(i)

    	
    	 /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
    
            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
    
            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
    
                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
    	
    复制代码
  • rehash()

            /**
         * Re-pack and/or re-size the table. First scan the entire
         * table removing stale entries. If this doesn't sufficiently
         * shrink the size of the table, double the table size.
         */
        private void rehash() {
            expungeStaleEntries();
    
            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }
    
    
    
    
    复制代码
  1. expungeStaleEntries()

    	/**
         * Expunge all stale entries in the table.
         */
        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);
            }
        }
    
    
    复制代码
  2. resize()

    	/**
         * Double the capacity of the table.
         */
        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];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
    
            setThreshold(newLen);
            size = count;
            table = newTab;
        }
    
    
    
    
    复制代码
  • 回到for循环体中的内容

    	 for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
    
                if (k == key) {
                    e.value = value;
                    return;
                }
    
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
    
    复制代码

小结

  1. 阅读源码需要读注释。注释甚至比源码实现细节更加重要,因为源码注释讲述的是为什么有这个方法以及一些注意事项。不然,只是知其然,而不知其所以然。不仅获取不到好处,反而受到它的牵累。

  2. 当有足够的基础知识了,比如理解ThreadLocal的工作过程,理解魔数相关知识,理解环形数组结构,理解解决hash碰撞原理,那么读源码实现细节,就是一件自然而然的事情。

猜你喜欢

转载自juejin.im/post/7083373462625976333