Java讀源碼之ThreadLocal

  • 2019 年 10 月 15 日
  • 筆記

前言

JDK版本: 1.8

之前在看Thread源碼時候看到這麼一個屬性

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal實現的是每個線程都有一個本地的副本,相當於局部變量,其實ThreadLocal就是內部自己實現了一個map數據結構。

ThreadLocal確實很重要,但想到看源碼還是有個小故事的,之前去美團點評面試,問我如何保存用戶登錄token,可以避免層層傳遞token?

心想這好像是在說ThreadLocal,然後開始胡說放在redis里或者搞個ThreadLocal,給自己挖坑了

面試官繼續問,ThreadLocal使用時候主要存在什麼問題么?

完蛋,確實只了解過,沒怎麼用過,涼涼,回來查了下主要存在的問題如下

  • ThreadLocal可能內存泄露?

帶着疑惑進入源碼吧

源碼

類聲明和重要屬性

package java.lang;    public class ThreadLocal<T> {        // hash值,類似於Hashmap,用於計算放在map內部數組的哪個index上      private final int threadLocalHashCode = nextHashCode();      private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}      // 初始0      private static AtomicInteger nextHashCode = new AtomicInteger();      // 神奇的值,這個hash值的倍數去計算index,分佈會很均勻,總之很6      private static final int HASH_INCREMENT = 0x61c88647;        static class ThreadLocalMap {            // 注意這是一個弱引用          static class Entry extends WeakReference<ThreadLocal<?>> {              Object value;                Entry(ThreadLocal<?> k, Object v) {                  super(k);                  value = v;              }          }          // 初始容量16,一定要是2的倍數          private static final int INITIAL_CAPACITY = 16;          // map內部數組          private Entry[] table;          // 當前儲存的數量          private int size = 0;          // 擴容指標,計算公式 threshold = 總容量 * 2 / 3,默認初始化之後為10          private int threshold;

增改操作

讓我們先來看看增改方法

public void set(T value) {      Thread t = Thread.currentThread();      // 拿到當前Thread對象中的threadLocals引用,默認threadLocals值是null      ThreadLocalMap map = getMap(t);      if (map != null)          // 如果ThreadLocalMap已經初始化過,就把當前ThreadLocal實例的引用當key,設置值          map.set(this, value); //下文詳解      else          // 如果不存在就創建一個ThreadLocalMap並且提供初始值          createMap(t, value);  }    ThreadLocalMap getMap(Thread t) {      return t.threadLocals;  }    void createMap(Thread t, T firstValue) {      t.threadLocals = new ThreadLocalMap(this, firstValue);  }

讓我們來看看map.set(this, value)具體怎麼操作ThreadLocalMap

private void set(ThreadLocal<?> key, Object value) {      // 獲取ThreadLocalMap內部數組      Entry[] tab = table;      int len = tab.length;      // 算出需要放在哪個桶里      int i = key.threadLocalHashCode & (len-1);      // 如果當前桶衝突了,這裡沒有用拉鏈法,而是使用開放定指法,index遞增直到找到空桶,數據量很小的情況這樣效率高      for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {          // 拿到目前桶中key          ThreadLocal<?> k = e.get();          // 如果桶中key和我們要set的key一樣,直接更新值就ok了          if (k == key) {              e.value = value;              return;          }          // 桶中key是null,因為是弱引用,可能被回收掉了,這個時候我們直接佔為己有,並且進行cleanSomeSlots,當前key附近局部清理其他key是空的桶          if (k == null) {              replaceStaleEntry(key, value, i);              return;          }      }      // 如果沒衝突直接新建      tab[i] = new Entry(key, value);      int sz = ++size;      // 當前key附近局部清理key是空的桶,如果一個也沒清除並且當前容量超過閾值了就擴容      if (!cleanSomeSlots(i, sz) && sz >= threshold)          rehash();  }      private void rehash() {      // 這個方法會清除所有key為null的桶,清理完後size的大小會變小      expungeStaleEntries();        // 此時size還大於閾值的3/4就擴容      if (size >= threshold - threshold / 4)          // 2倍擴容          resize();  }

為什麼會內存泄漏

總算讀玩了set,大概明白了為什麼會發生內存泄漏,畫了個圖

ThreadLocalMap.Entry中的key保存了ThreadLocal實例的一個弱引用,如果ThreadLocal實例棧上的引用斷了,只要GC一發生,就鐵定被回收了,此時Entry的key,就是null,但是呢Entry的value是強引用而且是和Thread實例生命周期綁定的,也就是線程沒結束,值就一直不會被回收,所以產生了內存泄漏。

總算明白了,為什麼一個set操作要這麼多次清理key為null的桶。

既然這麼麻煩,為什麼key一定要用弱引用?

繼續看上面的圖,如果我們的Entry中保存的是ThreadLocal實例的一個強引用,我們刪掉了ThreadLocal棧上的引用,同理此時不僅value就連key也不會回收了,這內存泄漏就更大了

查詢操作

public T get() {      Thread t = Thread.currentThread();      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();  }    private T setInitialValue() {      T value = initialValue();      Thread t = Thread.currentThread();      ThreadLocalMap map = getMap(t);      if (map != null)          // 如果只是threadLocals.Entry是空,就設置value為null          map.set(this, value);      else          // 如果threadLocals是空,就new 一個key是當前ThreadLocal,value是空的ThreadLocalMap          createMap(t, value);      return value;  }    protected T initialValue() {      return null;  }

讓我們來看看map.getEntry(this)具體怎麼操作ThreadLocalMap

private Entry getEntry(ThreadLocal<?> key) {      int i = key.threadLocalHashCode & (table.length - 1);      Entry e = table[i];      if (e != null && e.get() == key)          // 最好情況,定位到了Entry,並且key匹配          return e;      else          // 可能是hash衝突重定址了,也可能是key被回收了          return getEntryAfterMiss(key, i, e);  }    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {      Entry[] tab = table;      int len = tab.length;      // 向後遍歷去匹配key,同時清除key為null的桶      while (e != null) {          ThreadLocal<?> k = e.get();          if (k == key)              return e;          if (k == null)              expungeStaleEntry(i);          else              i = nextIndex(i, len);          e = tab[i];      }      return null;  }

如何避免內存泄漏

新增,查詢中無處不在的去清理key為null的Entry,是不是我們就可以放心了,大多數情況是的,但是如果我們在使用線程池,核心工作線程是不會停止的,會重複利用,這時我們的Entry中的value就永遠不會被回收了這很糟糕,還好源碼作者還沒給我提供了remove方法,綜上所述,養成良好習慣,只要使用完ThreadLocal,一定要進行remove防止內存泄漏

public void remove() {      ThreadLocalMap m = getMap(Thread.currentThread());      if (m != null)          m.remove(this);  }    private void remove(ThreadLocal<?> key) {      Entry[] tab = table;      int len = tab.length;      int i = key.threadLocalHashCode & (len-1);      for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {          if (e.get() == key) {              // 主要多了這一步,讓this.referent = null,GC會提供特殊處理              e.clear();              expungeStaleEntry(i);              return;          }      }  }