关于后端:查漏补缺ThreadLocal源码

1次阅读

共计 7721 个字符,预计需要花费 20 分钟才能阅读完成。

该提前剖析的都剖析完了,当初,来看一下 ThreadLocal, 原本还想还顺带学习一下 Netty 封装的 FastThreadLocal,然而写的有点多了,前面抽时间再写一篇了

What

ThreadLocal也是日常开发中比拟罕用的,他的凝视就很好的诠释了他是干什么用的。简略的说,ThreadLocal能够看作线程的公有变量。须要留神的是,他并不是用来解决共享变量的,上面会进行剖析

/*This class provides thread-local variables.  These variables differ from
 their normal counterparts in that each thread that accesses one (via its
 {@code get} or {@code set} method) has its own, independently initialized
 copy of the variable.  {@code ThreadLocal} instances are typically private
 static fields in classes that wish to associate state with a thread (e.g.,
 a user ID or Transaction ID).
  ... 略
 */
 /**
这个类提供线程局部变量。这些变量与其失常的对应形式不同,因为拜访一个的每个线程(通过其 get 或 set 办法)都有本人独立初始化的变量正本。ThreadLocal 实例通常是心愿将状态与线程关联的类中的公有动态字段(例如,用户 ID 或事务 ID)。*/

Details

createMap

简略将一下下源码,当调用 set() 办法塞值的时候呢,会创立 ThreadLocalMap 并赋值给线程的 threadLocalsThreadLocalMap 外部实现是通过 Entry 数组,通过哈希算出地位,Entry自身继承弱援用,key 封装为弱援用,value 则是强援用。

所以实践上 key 删了,value 援用还在

    public void set(T value) {
        // 获取以后线程
        Thread t = Thread.currentThread();
        // 获取 ThreadLocalMap 1.1
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 塞值,ThreadLocal 作为 key
            map.set(this, value);
        else
            // 创立 map1.2
            createMap(t, value);
    }
    
    //1.1
    ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
    
    //1.2
    void createMap(Thread t, T firstValue) {
        //2.1
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    //2.1ThreadLocalMap 构造方法
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // 创立 Entry 数组
        table = new Entry[INITIAL_CAPACITY];
        // 位运算算出放在哪
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        // 塞值 2.2
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
    
    //2.2
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

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

## set
这一段承接下面的 set 办法,离开成两个局部,独自讲一下,下面看到了,ThreadLocalMap是一个 Entry 数组,它没有像 HashMap 数组 + 链表 / 红黑树的一个构造,他是通过 hash 运算算进去要放在哪,那么如果你线程中使用了很多ThreadLocal,那么必定会遇到坑位被占的状况,既然是数组,也就只能向后顺延,找到 null 的地位,然而后面提到了,弱援用的问题,后面一篇,剖析了会被 GC 回收的问题,那么 key 就是 null,然而 value 的援用还是在的,这种应该要怎么解决,就是感觉这部分太多,顺便和下面离开来。

  private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        // 遍历下一个 entry 为 null 的状况
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();
            // 便当是从 i 顺次向后便当,如果有相等的状况就间接赋值了
            if (k == key) {
                e.value = value;
                return;
            }
            //key 被 GC 回收的状况
            if (k == null) {
                //1.1
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 直到遍历到数组中的 null 的状况
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            //1.2
            rehash();}
    
    //1.1
    /**
    * key:咱们传进来的 ThreadLocal
    * value:咱们穿进来的 value
    * staleSlot:外层循环中数组角标,不会变!* 这个地位的 entry 的 key 曾经被 GC 回收了
    */
    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                     int staleSlot) {Entry[] tab = table;
          int len = tab.length;
          Entry e;

          //slotToExpunge 革除起始地位的意思
          int slotToExpunge = staleSlot;
          // 间接从 staleSlot 的前一个地位开始
          // 这一段就是找到前一个 entry 为 null 的地位停,slotToExpunge 记录最开始曾经生效的 entry 的下标
          for (int i = prevIndex(staleSlot, len);
               (e = tab[i]) != null;
               i = prevIndex(i, len))
              if (e.get() == null)
                  slotToExpunge = i;

          // 从 staleSlot 的后一个地位开始
          for (int i = nextIndex(staleSlot, len);
               (e = tab[i]) != null;
               i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();
              // 如果 key 相等,阐明,以后地位的 entry 未生效,staleSlot 地位的 entry 生效,可能是后面槽位不够放过来的,或者是 hash 碰撞占了这个地位的,理论咱们的真正的 entry 因为 hash 碰撞放在了前面,此时就是替换
                // 将 staleSlot 地位生效的 entry 放到 i 以后地位,将咱们完整的 entry 拿过去
              if (k == key) {
                  e.value = value;

                  tab[i] = tab[staleSlot];
                  tab[staleSlot] = e;

                  // 如果最开始过期的地位就是 staleSlot 地位,此时曾经被置换到 i 地位,就是从 i 地位开始清理
                  if (slotToExpunge == staleSlot)
                      slotToExpunge = i;
                  // 从 slotToExpunge 地位开始革除 entry
                    //2.1,2.2
                  cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                  return;
              }
              // 清理 staleSlot 地位
              if (k == null && slotToExpunge == staleSlot)
                  slotToExpunge = i;
          }
          // 革除
          tab[staleSlot].value = null;
          tab[staleSlot] = new Entry(key, value);

          // 革除
          if (slotToExpunge != staleSlot)
              cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
      }

              //2.1
      private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;
          int len = tab.length;

          // 次要就是革除援用,等 GC 能够革除
          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 {
                    // 从新 hash,如果扩容,可能不在这个地位上,不在这个地位就革除,放到扩容后的地位上
                  int h = k.threadLocalHashCode & (len - 1);
                  if (h != i) {tab[i] = null;
                      while (tab[h] != null)
                          h = nextIndex(h, len);
                      tab[h] = e;
                  }
              }
          }
          return i;
      }
              /**
        2.2
              * i: 数组中,staleSlot 地位后的 null 的地位
              */
      private boolean cleanSomeSlots(int i, int n) {
          boolean removed = false;
          Entry[] tab = table;
          int len = tab.length;
            // 最多执行位数次
          do {i = nextIndex(i, len);
              Entry e = tab[i];
              if (e != null && e.get() == null) {
                  n = len;
                  removed = true;
                    // 就是下面那个 2.1
                  i = expungeStaleEntry(i);
              }
          } while ((n >>>= 1) != 0);
          return removed;
      }
      

中场劳动队下面增加 key 做一个总结

  1. 如果找到同样的 key,间接替换
  2. 如果 entry 过期,key 为 null(这里为了不便记录以后过期地位为 X)

    1. 如果理论匹配的 key(记录这个地位为 Y),Y 挪动到 x 地位,X 挪动到 Y,并从 X 以后所在位置开始清理过期 entry
    1. 如果到下一个 null 的 entry 地位为止,没有找到对应的 key,就把要塞的值塞在 x 地位,并开始清理过期 entry(这里有多重状况,看主食)
  3. 如果间接找到 null 地位,就间接塞值

既然是个数组,那么久必定会有扩容问题,也是差不多一样的,到达某一阀值久开始扩容,既然扩容,HashMap同一地位上哈希值雷同,要么再原位,要么再另外一个地位,移过去,持续组成链表完事,这里呢,是算出新地位,从那个地位向后找空位,就不过多赘述了,看下正文吧 ^_^

      //1.2,懒得讲了,也没 hashmap 简单,就是清理下过期的 entry,塞到新地位
      // 从新 hash
      private void rehash() {
          //3.1
          expungeStaleEntries();

          // Use lower threshold for doubling to avoid hysteresis
          if (size >= threshold - threshold / 4)
              //3.2
              resize();}
      //3.1
      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)
                  // 下面 2.1
                  expungeStaleEntry(j);
          }
      }
      //3.2
      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;
      }

get

    public T get() {Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //1.1
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {@SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();}
    
    //1.1
    private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        // 能匹配到间接返回
        if (e != null && e.get() == key)
            return e;
        else
            //2.1
            return getEntryAfterMiss(key, i, e);
    }
    //2.1 
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;
            int len = tab.length;

            while (e != null) {ThreadLocal<?> k = e.get();
                // 如果找到间接返回
                if (k == key)
                    return e;
                // 如果过期,革除    
                if (k == null)
                    // 下面 2.1
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            // 找不到返回空
            return null;
        }

Why

为啥 Entry 继承自 WeakReference

在 stackoverflow 上找到了这个答复。这里就不要脸的贴过来了。(能够过来点个赞)

设想一下。您在程序中应用线程池。为防止并发谬误,您常常应用 ThreadLocal。当一个线程实现工作时,你心愿 GC 开释 ThreadLocal 对象。因而,在您的线程中将其设置为 null。

好的。当初有两种状况。A 和 B。

A:假如 ThreadLocalMap 中有对 ThreadLocal 的强援用。ThreadLocal 对象不会被回收。咱们必须等到线程对象被回收后能力回收 ThreadLocal 对象。然而,坏消息是线程池常常重用曾经创立的 Thread 对象,而不是在线程实现其工作后回收它们。后果,咱们有越来越多的 ThreadLocal 对象无奈回收,直到 JVM 没有更多可用内存并抛出 OutOfMemoryError,咱们的程序解体。

B:因为 Entry 继承了 WeakReference,所以 TreadLocalMap 中对 ThreadLocal 对象的援用是弱援用。当线程实现工作时,咱们将线程中 ThreadLocal 的援用设置为 null。此时,ThreadLocal 对象在 ThreadLocalMap 中只有一个弱援用。当 GC 扫描 ThreadLocal 对象,发现只有一个弱援用时,就疏忽弱援用,回收 ThreadLocal 对象。这就是咱们想要的。这就是 Entry 扩大 WeakReference 的起因。

OOM 问题

频繁 set get 是会革除有效 entry 的,然而,如果线程生命周期很长,就会回关了,此时 key=null,value 始终有援用,革除不掉。始终占着一块内存,容易产生 OOM

始终占用着援用的小 demo。所以啊,多用remove

private static void test1() throws InterruptedException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        int size=1000000;
        WeakReference<ThreadLocal<Element>>[] list=new WeakReference[size];
        for (int i = 0; i < size; i++) {ThreadLocal<Element> threadLocal=new ThreadLocal<>();
            threadLocal.set(new Element(10));
            WeakReference<ThreadLocal<Element>> weakReference=new WeakReference<>(threadLocal);
            list[i]=weakReference;
        }
        System.out.println("------");
        mustGC();


        for (int i = 0; i < size; i++) {WeakReference<ThreadLocal<Element>> weakReference=list[i];
            ThreadLocal<Element> threadLocal=weakReference.get();
            if(threadLocal!=null){Element demo=threadLocal.get();
                if(demo!=null){System.out.println(i);
                }
            }
        }
        Thread t=Thread.currentThread();


        System.out.println();}
    private static void  mustGC() throws InterruptedException {System.gc();
        Thread.sleep(1000);
    }

other

集体感觉,就是个和线程绑定的变量,比拟适宜单个服务的 && 和线程绑定的变量,比方日志跟踪号,事务等。实际上源码也是这么解决的。

哎,祝贺石头人冠军,太强了,Wings 咒骂 5 年了,该偿还完结了吧

本文由 mdnice 多平台公布

正文完
 0