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 开释。