ThreadLocal詳解

???????ThreadLocal在java.lang包中,其主要作用是提供一個和線程綁定的變量環境,即通過ThreadLocal在一個線程中存儲了一個變量之后,再在另一個線程中使用同一個ThreadLocal對象設置值,第二個線程內設置的值不會將第一個線程內設置的值覆蓋,并且在同一個線程中可以獲取之前設置的值。如下是一個ThreadLocal的使用示例:

public class ThreadLocalExample {
  public static void main(String[] args) {
    ThreadLocal<String> threadLocal = new MyThreadLocal<>();
    Runnable task1 = () -> {
      threadLocal.set("task1");
      sleep(2);
      threadLocal.get();
    };

    Runnable task2 = () -> {
      sleep(1);
      threadLocal.set("task2");
      sleep(2);
      threadLocal.get();
    };

    Thread thread1 = new Thread(task1);
    Thread thread2 = new Thread(task2);
    thread1.start();
    thread2.start();
  }

  private static void sleep(int seconds) {
    try {
      TimeUnit.SECONDS.sleep(seconds);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  private static final class MyThreadLocal<T> extends ThreadLocal<T> {
    @Override
    public T get() {
      T result = super.get();
      System.out.println(Thread.currentThread().getName() + " invoke get method, result is " + result);
      return result;
    }

    @Override
    public void set(T value) {
      System.out.println(Thread.currentThread().getName() + " invoke set method, value is " + value);
      super.set(value);
    }
  }
}

???????如下是上述代碼的執行結果:

Thread-0 invoke set method, value is task1
Thread-1 invoke set method, value is task2
Thread-0 invoke get method, result is task1
Thread-1 invoke get method, result is task2

???????可以看到,Thread-0首先往ThreadLocal中設置了一個值,接著Thread-1也設置了一個值,但是Thread-1并沒有將Thread-0設置的值覆蓋,因為接下來Thread-0從ThreadLocal中獲取的值還是其先前設置的值,并且Thread-1獲取的值也是其先前設置的值。

???????在項目中,借助于ThreadLocal我們可以編寫出一些非常優雅的代碼,并且實現一些類似于緩存的功能,比如如下util類:

public class ParamUtil {
  private static final ThreadLocal<Map<String, Object>> params = new ThreadLocal<>();

  public static void clear() {
    params.remove();
  }

  public static void setParam(String key, Object obj) {
    Map<String, Object> paramsMap = params.get();
    if (null == paramsMap) {
      paramsMap = new HashMap<>();
      params.set(paramsMap);
    }
    paramsMap.put(key, obj);
  }
  
  public static <T> T getParam(String key) {
    Map<String, Object> paramMap = params.get();
    if (paramMap == null) {
      return null;
    }

    @SuppressWarnings("unchecked") T result = (T) paramMap.get(key);
    return result;
  }
}

???????在ParamUtil中,我們聲明了一個ThreadLocal類型的變量params,其存儲的是一個Map<String, Object>類型的數據,也就是我們實際存儲的數據是放在這個map中的,這里的Map使用HashMap即可,因為ThreadLocal針對每個線程都是保存有其存儲的變量的一個副本,因而針對每個線程其都有一個Map對象,也就不存在并發的問題,如下是該util類的使用示例:

@Service
public class MlsOrgServiceImpl implements MlsOrgService {
  @Autowired
  private MlsOrgDao mlsOrgDao;
  
  @Override
  public MlsOrgInfo getMlsOrg(Long id) {
    MlsOrgInfo mlsOrgInfo = ParamUtil.getParam("mlsOrgInfo");
    if (null == mlsOrgInfo) {
      mlsOrgInfo = mlsOrgDao.getByMlsOrgId(id);
      ParamUtil.setParam("mlsOrgInfo", mlsOrgInfo);
    }
    
    return mlsOrgInfo;
  }
}

???????這里在service方法中直接從ParamUtil獲取緩存的數據,如果存在,則直接返回,如果不存在,則從dao從查詢,并且將其設置到ParamUtil中。這里需要說明的是,通過這種方式進行緩存有三個優點:

  • 緩存實效性較好。因為用戶的一次請求的時間非常短,因而該緩存只會在這一次請求中有效,實時更改數據庫中的數據對后續的請求都是生效的;
  • 重復調用時效果明顯。當需要緩存的信息在請求中需要經常用到的時候該緩存的效果將非常明顯;
  • 可以跨多層調用。在Java web項目中,相較于將數據緩存在request中,ThreadLocal可以跨多個層(controller,service等)進行緩存,而request一般只用于controller層中;

???????這里需要說明一點是,使用ThreadLocal進行緩存的時候,由于其是和線程綁定的,而服務端框架中,不會每次請求都新建一個線程進行處理,而是有空余線程時則復用該線程處理新的請求,因而這種緩存方式需要在每次請求開始時(比如Java web中的攔截器)對ThreadLocal中存儲的數據進行清理,這樣可以避免當前請求中獲取到了之前某次請求中緩存的數據。

???????通過上述示例可以看出,ThreadLocal主要有兩個方法:get和set方法。get方法用于獲取其存儲的值,set方法則將數據存儲在ThreadLocal中。ThreadLocal在底層維護了一個Map對象,其鍵是一個ThreadLocal對象,而值則為該ThreadLocal對象中存儲的值,在調用ThreadLocal的get和set方法的時候實際上底層調用的是該map對象的對應方法。并且ThreadLocal實現將數據與線程綁定的方式則主要是將這個Map對象實例保存在每個Thread對象中,如下所示Thread類中的Map對象的聲明:

ThreadLocal.ThreadLocalMap threadLocals = null;

???????首先我們看看ThreadLocal的set方法實現,如下是其代碼:

public void set(T value) {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
}

???????可以看到,每次調用set方法時,其都會首先獲取當前執行該set方法的線程,然后獲取該線程中保存的ThreadLocalMap實例,如果該map不為空,則將當前ThreadLocal和其值設置到該map中,否則創建一個ThreadLocalMap實例,然后將當前設置的值初始化到該map中,并且還會將該實例設置到當前線程的ThreadLocalMap實例中。如下是getMap(Thread)和createMap(Thread, T)方法的實現:

ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

???????在講解ThreadLocalMap.set(ThreadLocal, T)方法之前,我們首先看看ThreadLocalMap的數據結構及其存儲方式:

static class ThreadLocalMap {
  static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
    }
  }
  
  private static final int INITIAL_CAPACITY = 16;
  private Entry[] table;
  private int size = 0;
  private int threshold;
  
  private void setThreshold(int len) {
    threshold = len * 2 / 3;
  }

  private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
  }

  private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
  }
}

???????這里列出了ThreadLocalMap的主要屬性以及其余比較簡單的方法。可以看出,ThreadLocalMap底層和HashMap類似,是使用一個Entry類型的數組來存儲鍵值對的,但是不同的是其Entry是繼承自WeakReference的,值為Object類型的一個屬性。WeakReference的作用是其可以存儲一個引用,如果在其他位置(比如某個全局或局部變量)沒有存儲相同的引用,那么Java垃圾回收機制則會對該引用對象進行回收。這里使用WeakReference的用意則在于線程是可消逝的,那么當其消逝之后和其綁定的ThreadLocal對象如果沒有引用到則可以被垃圾回收機制回收。回過頭來,ThreadLocalMap中的另外幾個屬性的意義如下:INITIAL_CAPACITY表示Entry數組的默認初始化長度,size存儲了當前鍵值對的數量,threshold存儲了當前Entry中最多存儲的鍵值對數目,超過該數目時就會對當前ThreadLocalMap進行rehash()操作,通過setThreshold(int)方法可以看出,當前Map的默認負載銀子是2/3。

???????在ThreadLocalMap中,其Entry只有兩個屬性:鍵和值。相較于HashMap,其在當前Entry中保存有一個Entry類型的next指針,即使用一個單向鏈表的方式來解決hash沖突。ThreadLocalMap的Entry由于只有兩個屬性,因而其解決沖突的方式不是使用單向鏈表的方式,并且由于每個Entry對象也是一個WeakReference實例,這也導致其不能使用單項鏈表的方式解決沖突。這里ThreadLocalMap使用的是線性探測再散列法解決hash沖突的,即當一個鍵映射到某個槽位之后,其會先檢查該槽位是否存儲有值,如果有值則檢查數組的下一位是否有值,如此查找直到找到一個沒有存儲數據的槽位將當前鍵值對存儲其中。這里實際探測時,如果發生沖突,其還會檢查當前槽位的Entry的鍵是否為null,因為其可能被垃圾回收機制給回收,如果為空則將當前鍵值對存儲于該槽位中,并且還會對從該槽位到后續第一個為空的槽位的沒有被回收的鍵值對進行再散列,并且清除已經被回收的鍵值對,這樣做的目的有助于減少hash表中鍵值對的數量,減少發生沖突的概率和rehash的次數。

???????下面我們通過ThreadLocalMap.set()方法具體看看其是如何存儲鍵值對的。如下是set()方法的具體實現:

private void set(ThreadLocal<?> key, Object value) {
  Entry[] tab = table;
  int len = tab.length;
  int i = key.threadLocalHashCode & (len-1);    // 對數組長度取余計算將存儲的槽位

  // 從計算得到的槽位開始依次往后遍歷,直到找到對應的鍵或者是遇到了null槽位
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();

    // 如果k == key說明之前存在該鍵及其值,那么直接替換其值為新的值即可
    if (k == key) {
      e.value = value;
      return;
    }

    // 如果k為空,說明當前entry的鍵已經被垃圾回收機制回收了,那么將要設置的鍵值對替換當前鍵值對
    // 并且對其后沒有被回收的鍵值對進行再散列
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }

  // 在for循環中沒有找到當前key對應的值,說明之前沒有設置相同鍵的鍵值對
  // 此時i指向的是沖突槽位之后第一個為空的槽位,那么在該槽位上新建一個entry存儲當前鍵值對
  tab[i] = new Entry(key, value);
  int sz = ++size;
  
  // cleanSomeSlots的作用是選擇性的對一些槽位進行檢測,如果其已經被回收,則對其進行清理
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

???????總結ThreadLocalMap.set()方法,其首先會計算當前ThreadLocal對象將要存儲的位置,然后使用一個for循環從該位置處往后依次遍歷,檢查每一個鍵值對是否已經被回收,被回收了則將當前的鍵值對存儲于該位置,或者是判斷當前鍵是否就是要存儲的鍵,相等則將當前當前鍵對應的值替換為新的值。這里我們看看replaceStaleEntry()方法的具體實現:

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
  Entry[] tab = table;
  int len = tab.length;
  Entry e;

  int slotToExpunge = staleSlot;
  // 從被回收的槽位開始往前遍歷查找第一個已經被回收的鍵值對
  for (int i = prevIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = prevIndex(i, len))
    if (e.get() == null)
      slotToExpunge = i;

  // 從被回收的槽位開始向后遍歷,查找是否有鍵為當前要設置的鍵,有則將其與被回收的槽位進行替換
  // 并且對后續第一個被回收的槽位開始的后續元素進行再散列,因為第一個被回收的槽位將重置為空
  for (int i = nextIndex(staleSlot, len);
       (e = tab[i]) != null;
       i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();

    // 在被回收的槽位之后找到與要設置的鍵相同的鍵,那么將其值替換為新值,并且與被回收的槽位替換位置
    if (k == key) {
      // 更新其值為新值
      e.value = value;

      // 替換槽位
      tab[i] = tab[staleSlot];
      tab[staleSlot] = e;

      // slotToExpunge記錄的是被回收的槽位(staleSlot)之后第一個被回收的槽位
      if (slotToExpunge == staleSlot)
        slotToExpunge = i;
      // 對被回收的槽位之后第一個被回收的槽位之后的元素進行再散列,
      // 因為該第一個被回收的槽位將被重置為空
      cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
      return;
    }

    // 記錄被回收的槽位之后第一個被回收的槽位
    if (k == null && slotToExpunge == staleSlot)
      slotToExpunge = i;
  }

  // 如果在被回收的槽位之后沒有找到與要設置的鍵相同的鍵,那么直接新建一個entry替換被回收的槽位
  tab[staleSlot].value = null;
  tab[staleSlot] = new Entry(key, value);

  // 如果slotToExpunge != staleSlot則說明被回收的槽位之后有被回收的鍵值對,那么就從該槽位開始
  // 對后續元素進行回收或者再散列
  if (slotToExpunge != staleSlot)
    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

???????總結來說,replaceStaleEntry()方法主要是將要設置的鍵值對替換為當前已經被回收的鍵值對,并且其會在當前被回收的鍵值對之后找到第一個被回收的鍵值對,找到了則將其重置為空,并且對其后的數據進行再散列。這里再散列的工作是通過expungeStaleEntry()方法實現的,我們可以看看該方法是如何實現的:

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

  // 對已經被回收的鍵值對進行重置
  tab[staleSlot].value = null;
  tab[staleSlot] = null;
  size--;

  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;

        while (tab[h] != null)  // 查找目標槽位之后第一個可以存儲數據的槽位
          h = nextIndex(h, len);
        tab[h] = e;
      }
    }
  }
  return i;
}

???????以上是ThreadLocalMap.set()方法的具體實現,通過上面的說明可以看出,其會經常檢查槽位中的數據是否已經被回收,這樣做的目的是減少當前map中entry的數量,減少get和set方法需要檢查的entry的數量,并且也可以避免rehash操作的數量。這里也說明了線性探測再散列法的一個缺點,即其將數據直接存儲在數組上會大大增加沖突發生的數量,因而需要經常對槽位進行清理。至于為什么會增加沖突發生的數量,這也很好理解,比如對于key1,其計算的槽位是3,但是由于3,4和5號槽位都因為沖突存儲了數據,那么其只能存儲在6號槽位上。此時另一個key2計算的槽位是4,其會發現4號槽位已經存儲了數據,因而只能存儲在7號槽位上。可以發現,不同hash值的數據因為這種存儲方式而扎堆的存儲在了一個局部沖突塊中。

???????前面我們對ThreadLocalMap.set()方法實現方式進行了講解,下面我們來看看ThreadLocalMap.get()方法,其實現思路和set()方法非常類似,即確認要查找的key對應的槽位之后查找是否有key和當前key相同,相同則返回,否則一直查找直到遇到null槽位為止。期間其還會對找到的已經被回收的槽位進行處理。如下是ThreadLocalMap.get()方法的具體代碼:

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,則直接返回,否則會從該槽位開始往后查找,如下是getEntryAfterMiss()方法的代碼:

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)
      expungeStaleEntry(i);
    else
      i = nextIndex(i, len);
    e = tab[i];
  }
  return null;
}

????????在從目標槽位開始往后查找的過程中,其會判斷當前key是否為要查找的key,如果是則返回,不是則會判斷該key是否為空,如果為空則對其進行回收,并且對其后的鍵值對進行再散列。最后,如果沒有找到目標key,則返回空。

????????最后需要說明的一點是,上述ThreadLocalMap中的鍵值對為ThreadLocal對象及其存儲的值,而在每個Thread對象中都是有一個獨立的ThreadLocalMap對象的,這里講到的Map中的沖突解決等指的都是在同一個線程中創建了多個ThreadLocal對象時發生的,即在同一個線程中,其會把該線程中使用的所有ThreadLocal對象都存儲到同一個ThreadLocalMap對象中。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容