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

背景

  1. 推荐阅读ThreadLocal工作过程、魔数的学习和疑问思考、ThreadLocal解决Hash碰撞
  2. 探究ThreadLocal源码中核心方法set的工作过程及其实现细节。

过程

  • set的入口函数
    ThreadLocal源码阅读五:核心方法set源码探究_第1张图片
    注释含义:把当前线程中的本地变量值,设置到具体的容器entry[]中。大多数ThreadLocal的子类不要重写set方法去设置初始值。因为initialValue方法给线程本地变量设置值。

    细节

      1. 获取当前线程
      2. 获取当前线程属性ThreadLocalMap
      3. 判断当前线程属性ThreadLocalMap是否为null
      4. 如果不为空,则直接设置值
      5. 如果为空,则创建ThreadLocalMap
    
  • 创建ThreadLocalMap
    ThreadLocal源码阅读五:核心方法set源码探究_第2张图片
    注释含义:创建一个与ThreadLocal关联的map容器。

    细节

     1. 调用ThreadLocalMap的有参构建器
    

    ThreadLocal源码阅读五:核心方法set源码探究_第3张图片

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

    细节

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

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

    细节

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

    ThreadLocal源码阅读五:核心方法set源码探究_第5张图片

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


  • 不创建ThreadLocalMap
    ThreadLocal源码阅读五:核心方法set源码探究_第6张图片

    细节

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

    1. 此方法的注释
      ThreadLocal源码阅读五:核心方法set源码探究_第7张图片
      注释含义

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

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

    2. 此方法的源码
      ThreadLocal源码阅读五:核心方法set源码探究_第8张图片
      细节

      先不看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碰撞原理,那么读源码实现细节,就是一件自然而然的事情。

你可能感兴趣的:(并发编程艺术)