精通高併發與多線程,卻不會用ThreadLocal?

你們好,我是小菜,一個渴望在互聯網行業作到蔡不菜的小菜。可柔可剛,點贊則柔,白嫖則剛! 死鬼~看完記得給我來個三連哦!java

本文主要介紹 ThreadLocal 的使用

若有須要,能夠參考數組

若有幫助,不忘 點贊微信

微信公衆號已開啓,小菜良記,沒關注的同窗們記得關注哦!多線程

以前咱們有在併發系列中提到 ThreadLocal 類和基本使用方法,那咱們就來看下 ThreadLocal 到底是如何使用的!併發

ThreadLocal 簡介

概念

ThreadLocal 類是用來提供線程內部的局部變量。這種變量在多線程環境下訪問(getset 方法訪問)時能保證各個線程的變量相對獨立於其餘線程內的變量。 ThreadLocal 實例一般來講都是 private static 類型的,用於關聯線程和上下文。函數

做用

  • 傳遞數據

提供線程內部的局部變量。能夠經過 ThreadLocal 在同一線程,不一樣組件中傳遞公共變量。源碼分析

  • 線程併發

適用於多線程併發狀況下。性能

  • 線程隔離

每一個線程的變量都是獨立的,不會相互影響。學習

ThreadLocal 實戰

1. 常見方法

  • ThreadLocal ()

構造方法,建立一個 ThreadLocal 對象this

  • void set (T value)

設置當前線程綁定的局部變量

  • T get ()

獲取當前線程綁定的局部變量

  • void remove ()

移除當前線程綁定的局部變量

2. 爲何要使用 ThreadLocal

首先咱們先看一組併發條件下的代碼場景:

 @Data
 public class ThreadLocalTest {
  private String name;
 ​
  public static void main(String[] args) {
  ThreadLocalTest tmp = new ThreadLocalTest();
  for (int i = 0; i < 4; i++) {
  Thread thread = new Thread(() -> {
  tmp.setName(Thread.currentThread().getName());
  System.out.println(Thread.currentThread().getName() +
  "t 拿到數據:" + tmp.getName());
  });
  thread.setName("Thread-" + i);
  thread.start();
  }
  }
 }

咱們理想中的代碼輸出結果應該是這樣的:

 /** OUTPUT **/
 Thread-0  拿到數據:Thread-0
 Thread-1  拿到數據:Thread-1
 Thread-2  拿到數據:Thread-2
 Thread-3  拿到數據:Thread-3

可是實際上輸出的結果倒是這樣的:

 /** OUTPUT **/
 Thread-0  拿到數據:Thread-1
 Thread-3  拿到數據:Thread-3
 Thread-1  拿到數據:Thread-1
 Thread-2  拿到數據:Thread-2

順序亂了沒有關係,可是咱們能夠看到 Thread-0 這個線程拿到的值倒是 Thread-1

從結果中咱們能夠看出多個線程在訪問同一個變量的時候會出現異常,這是由於線程間的數據沒有隔離!

併發線程出現的問題?那加鎖不就完事了!這個時候你三下五除二的寫下了如下代碼:

 @Data
 public class ThreadLocalTest {
 ​
  private String name;
 ​
  public static void main(String[] args) {
  ThreadLocalTest tmp = new ThreadLocalTest();
  for (int i = 0; i < 4; i++) {
  Thread thread = new Thread(() -> {
  synchronized (tmp) {
  tmp.setName(Thread.currentThread().getName());
  System.out.println(Thread.currentThread().getName() 
  + "t" + tmp.getName());
  }
  });
  thread.setName("Thread-" + i);
  thread.start();
  }
  }
 }
 /** OUTPUT **/
 Thread-2 Thread-2
 Thread-3 Thread-3
 Thread-1 Thread-1
 Thread-0 Thread-0

從結果上看,加鎖好像是解決了上述問題,可是 synchronized 經常使用於多線程數據共享的問題,而非多線程數據隔離的問題。這裏使用 synchronized 雖然解決了問題,可是多少有些不合適,而且 synchronized 屬於重量級鎖,爲了實現多線程數據隔離貿然的加上 synchronized,也會影響到性能。

加鎖的方法也被否認了,那麼該如何解決?不如用 ThreadLocal 牛刀小試一番:

 public class ThreadLocalTest {
 ​
  private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 ​
  public String getName() {
  return threadLocal.get();
  }
 ​
  public void setName(String name) {
  threadLocal.set(name);
  }
 ​
  public static void main(String[] args) {
  ThreadLocalTest tmp = new ThreadLocalTest();
  for (int i = 0; i < 4; i++) {
  Thread thread = new Thread(() -> {
  tmp.setName(Thread.currentThread().getName());
  System.out.println(Thread.currentThread().getName() + 
  "t 拿到數據:" + tmp.getName());
  });
  thread.setName("Thread-" + i);
  thread.start();
  }
  }
 }

在查看輸出結果以前,咱們先來看看代碼發生了那些變化

首先多了一個 private static 修飾的 ThreadLocal ,而後在 setName 的時候,咱們其實是往 ThreadLocal 裏面存數據,在 getName 的時候,咱們是在 ThreadLocal 裏面取數據。感受操做上也是挺簡單的,可是這樣真的能作到線程間的數據隔離嗎,咱們再來看一看結果:

 /** OUTPUT **/
 Thread-1  拿到數據:Thread-1
 Thread-2  拿到數據:Thread-2
 Thread-0  拿到數據:Thread-0
 Thread-3  拿到數據:Thread-3

從結果上能夠看到每一個線程都能取到對應的數據。ThreadLocal 也已經解決了多線程之間數據隔離的問題。

那麼咱們來小結一下,爲何須要使用ThreadLocal,與 synchronized 的區別是什麼

  • synchronized

原理: 同步機制採用 "以時間換空間" 的方式,只提供了一份變量,讓不一樣線程排隊訪問

側重點: 多個線程之間同步訪問資源

  • ThreadLocal

原理: ThreadLocal 採用 "以空間換時間" 的方式,爲每一個線程都提供了一份變量的副本,從而實現同時訪問而互不干擾

側重點: 多線程中讓每一個線程之間的數據相互隔離

3. 內部結構

從上面的案例中咱們能夠看到 ThreadLocal 的兩個主要方法分別是 set()get()

那咱們不妨猜測一下,若是讓咱們來設計 ThreadLocal ,咱們該如何設計,是否會有這樣的想法:每一個 ThreadLocal 都建立一個 Map,而後用線程做爲 Mapkey,要存儲的局部變量做爲 Mapvalue ,這樣就能達到各個線程的局部變量隔離的效果。

這個想法也是沒錯的,早期的 ThreadLocal 即是這樣設計的,可是在 JDK 8 以後便更改了設計,以下:

設計過程:

  1. 每一個 Thread 線程內部都有一個 ThreadLocalMap
  2. ThreadLocalMap 中存儲着以 ThreadLocal 對象爲 key ,線程變量爲 value
  3. Thread 內部的 Map 是由 ThreadLocal 維護的,由 ThreadLocal 負責向 Map 設置和獲取線程的變量值
  4. 對於不一樣的線程,每次獲取副本值時,別的線程並不能獲取到線程的副本值,這樣就會造成副本的隔離,互不干擾

注: 每一個線程都要有本身的一個 map,可是這個類就是一個普通的 Java 類,並無實現 Map 接口,可是具備相似 Map 相似的功能。

經過這樣實現看起來貌似會比以前咱們猜測的更加複雜,這樣作的好處是什麼呢?

  • 每一個 Map 存儲的 Entry 數量就會變少,由於以前的存儲數量由 Thread 的數量決定,如今是由 ThreadMap 的數量決定,在實際開發中,ThreadLocal 的數量要更少於 Thread 的數量。
  • Thread 銷燬以後,對應的 ThreadLocalMap 也會隨之銷燬,能減小內存的使用

4. 源碼分析

首先咱們先看 ThreadLocalMap 中有哪些成員:

若是你看過 HashMap 的源碼,確定會以爲這幾個特別熟悉,其中:

  • INITIAL_CAPACITY:初始容量,必須是 2 的整次冪
  • table:存放數據的table
  • size:數組中 entries 的個數,用於判斷 table 當前使用量是否超過閾值
  • threshold:進行擴容的閾值,表使用量大於它的時候會進行擴容

ThreadLocals

Thread 類中有個類型爲 ThreadLocal.ThreadLocalMap 類型的變量 ThreadLocals ,這個就是用來保存每一個線程的私有數據。

ThreadLocalMap

ThreadLocalMapThreadLocal的內部類,每一個數據用Entry保存,其中的Entry用一個鍵值對存儲,鍵爲ThreadLocal的引用。

咱們能夠看到 Entry 繼承於WeakReference,這是由於若是是強引用,即便把 ThreadLocal 設置爲 nullGC 也不會回收,由於 ThreadLocalMap 對它有強引用。

在沒有手動刪除這個Entry以及CurrentThread依然運行的前提下,始終有強引用鏈 threadRef -> currentThread -> threadLocalMap -> entryEntry就不會被回收(Entry中包括了ThreadLocal實例和value),致使Entry內存泄漏。

那是否是就是說若是使用了弱引用,就不會形成內存泄露 呢,這也是不正確的。

由於若是咱們沒有手動刪除 Entry 的狀況下,此時 Entry 中的 key == null,這個時候沒有任何強引用指向 threaLocal 實例,因此 threadLocal 就能夠順利被 gc 回收,可是 value 不會被回收,而這塊的 value 永遠不會被訪問到,所以會致使內存泄露

接下來咱們看下 ThreadLocalMap 的幾個核心方法:

set 方法

首先咱們先看下源碼:

 public void set(T value) {
  // 獲取當前線程對象
  Thread t = Thread.currentThread();
  // 獲取此線程對象中維護的ThreadLocalMap對象
  ThreadLocalMap map = getMap(t);
  // 判斷map是否存在
  if (map != null)
  // 存在則調用map.set設置此實體entry
  map.set(this, value);
  else
  // 若是當前線程不存在ThreadLocalMap對象則調用createMap進行ThreadLocalMap對象的初始化
  // 並將 t(當前線程)和value(t對應的值)做爲第一個entry存放至ThreadLocalMap中
  createMap(t, value);
 }
 ​
 ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
 }
 ​
 void createMap(Thread t, T firstValue) {
  //這裏的this是調用此方法的threadLocal
  t.threadLocals = new ThreadLocalMap(this, firstValue);
 }

執行流程:

  • 首先獲取當前線程,並根據當前線程獲取一個 map
  • 若是獲取的 map 不爲空,則將參數設置到 map 中(當前 ThreadLocal 的引用做爲 key
  • 若是 Map 爲空,則給該線程建立 map ,並設置初始值

get 方法

源碼以下:

 public T get() {
  // 獲取當前線程對象
  Thread t = Thread.currentThread();
  // 獲取此線程對象中維護的ThreadLocalMap對象
  ThreadLocalMap map = getMap(t);
  // 若是此map存在
  if (map != null) {
  // 以當前的ThreadLocal 爲 key,調用getEntry獲取對應的存儲實體e
  ThreadLocalMap.Entry e = map.getEntry(this);
  // 對e進行判空 
  if (e != null) {
  @SuppressWarnings("unchecked")
  // 獲取存儲實體 e 對應的 value值
  // 即爲咱們想要的當前線程對應此ThreadLocal的值
  T result = (T)e.value;
  return result;
  }
  }
  return setInitialValue();
 }
 ​
 private T setInitialValue() {
  // 調用initialValue獲取初始化的值
  // 此方法能夠被子類重寫, 若是不重寫默認返回null
  T value = initialValue();
  // 獲取當前線程對象
  Thread t = Thread.currentThread();
  // 獲取此線程對象中維護的ThreadLocalMap對象
  ThreadLocalMap map = getMap(t);
  // 判斷map是否存在
  if (map != null)
  // 存在則調用map.set設置此實體entry
  map.set(this, value);
  else
  // 若是當前線程不存在ThreadLocalMap對象則調用createMap進行ThreadLocalMap對象的初始化
  // 並將 t(當前線程)和value(t對應的值)做爲第一個entry存放至ThreadLocalMap中
  createMap(t, value);
  // 返回設置的值value
  return value;
 }

執行流程:

  • 首先獲取當前線程,根據當前線程獲取一個 map
  • 若是獲取的 map 不爲空,則在 map 中以 ThreadLocal 的引用做爲 key 來在 map 中獲取對應的 Entry entry ,不然跳轉到第四步
  • 若是 Entry entry 不爲空 ,則返回 entry.value ,不然跳轉到第四步
  • map 爲空或者 entry 爲空,則經過 initialValue 函數獲取初始值 value ,而後用 ThreadLocal 的引用和 value 做爲 firstKeyfirstValue 建立一個新的 map

remove 方法

源碼以下:

 ​
 public void remove() {
  // 獲取當前線程對象中維護的ThreadLocalMap對象
  ThreadLocalMap m = getMap(Thread.currentThread());
  // 若是此map存在
  if (m != null)
  // 存在則調用map.remove
  m.remove(this);
 }
 // 以當前ThreadLocal爲key刪除對應的實體entry
 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) {
  e.clear();
  expungeStaleEntry(i);
  return;
  }
  }
 }

執行流程:

  • 首先獲取當前線程,並根據當前線程獲取一個 map
  • 若是得到的map 不爲空,則移除當前 ThreadLocal 對象對應的 entry

initialValue 方法

源碼以下:

 protected T initialValue() {
  return null;
 }

在源碼中咱們能夠看到這個方法僅僅簡單的返回了 null ,這個方法是在線程第一次經過 get () 方法訪問該線程的 ThreadLocal 時調用的,只有在線程先調用了 set () 方法纔不會調用 initialValue () 方法,一般狀況下,這個方法最多被調用一次。

若是們想要 ThreadLocal 線程局部變量有一個除 null 之外的初始值,那麼就必須經過子類繼承 ThreadLocal 來重寫此方法,能夠經過匿名內部類實現。

【END】

這篇 ThreadLocal 就介紹到這裏啦,但願讀到這裏的小夥伴可以有所收穫。

路漫漫,小菜與你一同求索!

看完不讚,都是壞蛋

今天的你多努力一點,明天的你就能少說一句求人的話!

我是小菜,一個和你一塊兒學習的男人。 💋

微信公衆號已開啓,小菜良記,沒關注的同窗們記得關注哦!

相關文章
相關標籤/搜索