乐趣区

关于java:ThreadLocal源码解析

ThreadLocal 如何实现线程间隔离,为什么 ThreadLocal 常常容易呈现内存溢出。带着这两个问题,在源码中找寻答案。
先从设置值开始,看 ThreadLocal.set() 如何实现的值保留。

    public void set(T value) {Thread t = Thread.currentThread();
        // 获取线程公有属性 threadLocals
        ThreadLocalMap map = getMap(t);
        if (map != null) {map.set(this, value);
        } else {createMap(t, value);
        }
    }
  • threadLocals: 线程 Thread 对象外部属性,这个属性默认就是 null,当初看来就是由 ThreadLocal.set 进行初始化的了。
    先不必管 ThreadLocalMap 如何实现,只有把他当作当成类型 Map 这类容器。一开始没有想明确线程应用变量不是一个 Object,而是一个容器。set 值时为什么不间接 ThreadLocal set val 间接给 Thread.threadLocals 而是将一个容器赋值。转念一想一个 Thread + 一个 ThreadLocal 只能保留一个 val,然而一个 Thread 能够对应多个 ThreadLocal,一个线程对象属性可能被多个 ThreadLocal 独特持有,差点被一个 Thread、一个 ThreadLocal 限度了思路。
    先看下 createMap 如何初始化的,value 如何保存起来。

      void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
      }
  • ThreadLocalMap: ThreadLocal 一个外部类,名字看起来像 Map 实现类,其实实质并没有和 Map 有任何关系,也没有实现 Map 接口。外部应用 Entry(相似 Map key-value 对象) 数组存储数据,应用 Hash 算法计算下标。如果呈现 hash 抵触如何解决呢,这个低配 Entry 并没有链表或者红黑树这样黑科技。

     static class ThreadLocalMap {
    
          /**
           *  应用弱援用包装 ThreadLocal 作为 map Key 
           *  在某些状况下 key 会被回收掉
           */
          static class Entry extends WeakReference<ThreadLocal<?>> {
              /** The value associated with this ThreadLocal. */
              Object value;
    
              Entry(ThreadLocal<?> k, Object v) {super(k);
                  value = v;
              }
          }
    
       
          ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];
             // 应用 hashCode 计算下标
            // 这里应用 hashCode 跟咱们一般对象不一样,通过自增长计算出来
              int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
              table[i] = new Entry(firstKey, firstValue);
              size = 1;
              setThreshold(INITIAL_CAPACITY);
          }
  • WeakReference: 弱援用的对象领有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具备弱援用的对象,不论以后内存空间足够与否,都会回收它的内存。
    因而一旦呈现 gc ThreadLocalMap 的 key 就会被回收掉,从而导致 ThreadLocal 设置 value 不能被删除,对象积压过多导致内存溢出。当初第二个问题失去答案了因为 map key 会被 gc 开释,从而导致 value 不能被删除,所以应用实现后必须手动开释掉 val。

          private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;
              int len = tab.length;
              int i = key.threadLocalHashCode & (len-1);
            // 循环外面解决 hash 抵触状况
              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) { // key 曾经被 gc
                     // 这个 i 不会变,只能向后找一位
                     // 这里会持续向后找,直到找到符合条件地位,并且将 key 被 gc  value 也回收掉,// 这个只有在肯定条件下才失效
                      replaceStaleEntry(key, value, i);
                      return;
                  }
              }
              // 当 hash 抵触时,会始终向后找,直到有空地位
              tab[i] = new Entry(key, value);
              int sz = ++size;
              // 在地位 i 前面搜查是否有 key 回收状况,则删除数组地位,返回 true 
              // 当有删除,不须要判断扩容状况了,一个新增对应删除,容量都没有减少
              if (!cleanSomeSlots(i, sz) && sz >= threshold)
                  rehash(); // 扩容数组}

    在呈现 hash 抵触时,只是将下标向后挪动,找到闲暇的地位。正如 set 办法正文上写,set 并不反对疾速 set、抵触了通过向后遍历数组找到空地位。
    ThreadLocalMap 竟然还有检测 key 为空机制,并且删除数组中地位。这个机制要肯定状况能力触发,首先被回收 key 必须在新增 key 前面地位能力被发现。
    看下 ThreadLocal.get 如何取值

      public T get() {Thread t = Thread.currentThread();
          // 调用 get 并不会初始化 threadLocals
          ThreadLocalMap map = getMap(t);
          if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {@SuppressWarnings("unchecked")
                  T result = (T)e.value;
                  return result;
              }
          }
          // 没有取到值,会返回 null
          return setInitialValue();}

    看下 ThreadLocalMap 外部如何将值返回的吧

          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
                  return getEntryAfterMiss(key, i, e);
          }
    
          // 向后查找合乎要去 key
          private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;
              int len = tab.length;
    
              while (e != null) { // 遇到 null 就停下来
                  ThreadLocal<?> k = e.get();
                  if (k == key)
                      return e;
                  if (k == null)
                       // k 曾经被 gc 了
                      // 在数组中删除这个地位,这样能够帮忙 value 回收了
                      expungeStaleEntry(i);
                  else
                      i = nextIndex(i, len);
                  e = tab[i];
              }
              return null;
          }

    这里 get 也不是一次性找到,会通过向后遍历匹配进去,这个以 HashMap 相比差距挺大的。插入、查找效率都在 N 之间。
    结尾两个纳闷,当初都失去解决了。
    不晓得大家是否和我好奇 ThreadLocalMap 的弱援用在应用时为什么不会被 gc 掉,导致传值进去而不能获取返回值呢,为什么要应用弱援用来包装 Key,如何权衡利弊的。
    做一个小例子

      public void  print(){WeakReference<Object> reference = getReference();
          System.gc();
          Object o = reference.get();
          System.out.println(o);
      }
    
      public WeakReference<Object> getReference(){WeakReference<Object> reference = new WeakReference(new Object());
          return reference;
      }

    print 办法会输入 null,这个就是我纳闷的中央,为什么 ThreadLocal 不会呈现下面在应用弱援用中,执行 gc 后,援用被回收导致 key 为空,取不到值了。
    在网上找到了答案,在应用一个例子验证下就会明确了

        @Test
      public void sidi() throws InterruptedException {getLocal();
          System.gc();
          TimeUnit.SECONDS.sleep(1);
          Thread thread = Thread.currentThread();
          System.out.println(thread); // 在这里打断点,察看 thread 对象里的 ThreadLocalMap 数据
    
      }
    
      private Local getLocal() {Local local = new Local();
          System.out.println(local.get());
          return local;
      }
    
      private static class Local {private ThreadLocal<String> local = ThreadLocal.withInitial(() -> "ssssssssss");
    
          public String get() {return local.get();
          }
    
          public void set(String str) {local.set(str);
          }

    上面看下 debug 后果图

    当初做一个小改变

    为什么有一个返回值后,ThreadLocal 就不会被 gc 回收掉呢,其实跟这个跟强援用有关系的。以后根对象就是 local 对象,持有 ThreadLocal<String> local,尽管 local 是被弱援用包装有可能被 gc 的,然而同时被以后 local 的强援用关联,对象依然是可达的,不会被垃圾回收掉。以后办法没有持有 local 时,Local 外部 local 是没有任何对象援用它,强援用并没有作用到 ThreadLocal,执行 gc 时必定会删除弱援用。总结执行 gc 时,弱援用总会被垃圾回收掉的,然而如果弱援用的对象同时被强援用持有,强援用作用域会盖过弱援用,在强援用可达之前,对象是不会被回收的。所以平时咱们在应用 ThreadLocal 时不会放心弱援用被删除状况,咱们在操作 ThreadLocal 时会必须持有它的对象援用,强援用保障了在以后持有对象代码里对象不会被回收。

看下面经典图片,实线示意强援用,虚线示意弱援用。当线程栈持有 ThreadLocal,作为 Entry key 的它不会被 gc,当 ThreadLocalRef 援用生效时,ThreadLocal 就会在下次 gc 时被回收掉。在持有 ThreadLocal 对象援用链时,ThreadLocal 弱援用都不会被回收的。
最初一个问题,为什么 ThreadLocal 要作为弱援用作为 ThreadLocalMap 外部 key 存在呢。咱们晓得 ThreadLocal 作为多线程操作本身公有变量工具类,本事不持有任何线程的变量,只是封装具体实现不便使用者调用。ThreadLocal 自身对象生命周期很短的,用完就能够回收了。试想下 ThreadLocal 作为 Entry key 存在,线程援用链就会变成 Thread->ThreadLocalMap ->ThreadLocal, 在线程对象不会开释之前,ThreadLoca 对象不会被回收的。如果咱们不手动开释它,是不是就和 Entry key 被删除一样,对象太多可能导致内存泄露。ThreadLocal 作为一个工具类没必要和 Thread 一起绑定,设置一个弱援用包装它,能够在对象作用域隐没后有利于垃圾回收器回收它。即便没有手动开释设置 value,也不用太过放心内存透露这块,在新增值、数组扩容时都会查看有 key 被 gc 状况,对 val 开释。

退出移动版