不再學ThreadLocal了,看這一篇就忘不掉了!(萬字總結)

爲何要學習ThreadLocal呢?由於面試官常常問,並且在線程中使用它能夠給咱們提供一個線程內的本地局部變量,這樣就能夠減小在一個線程中由於多函數之間的操做致使共享變量傳值的複雜性,說白了,咱們使用ThreadLocal能夠作到在一個線程內隨時隨地的取用,並且與其餘的線程互不干擾。java

在一些特殊的情景中,應用ThreadLocal會帶來極大的便利,不過不少人卻搞不懂Threadlocal究竟是個啥?在咱們的面試中也常常會被問到Threadlocal,因此基於咱們的實際應用以及應對面試,咱們都有必要好好的學習下Threadlocal。web

今天,咱們就來完完整整的學習下Threadlocal,爭取之後不再學了,由於看完今天這篇文章,你就對Threadlocal忘不了了!面試

一、什麼是Threadlocal?

首先,咱們既然要學習Threadlocal,那麼咱們先要知道它是個啥?咱們從名字來看,Threadlocal意思就是線程本地的意思,咱們這個屬於猜測,並不權威,那麼要想知道他是個啥,最好的辦法就是看看官方是怎麼定義它的,咱們看看ThreadLocal的源碼(基於jdk1.8)中對這個類的介紹:數組

This class provides thread-local variables. These variables differ from* their normal counterparts in that each thread that accesses one (via its* {@code get} or {@code set} method) has its own, independently initialized* copy of the variable. {@code ThreadLocal} instances are typically private* static fields in classes that wish to associate state with a thread (e.g.,* a user ID or Transaction ID).微信

這是在jdk1.8中對ThreadLocal這個類給的註釋,咱們簡單翻譯一下就是:數據結構

此類提供線程局部變量。這些變量與正常變量不一樣,由於每一個訪問一個線程(經過其{@code get}或{@code set}方法)的線程都有其本身的,獨立初始化的變量副本。 {@code ThreadLocal}實例一般是但願將狀態與線程相關聯的類中的私有靜態字段(例如用戶ID或交易ID)。ide

什麼意思呢?咱們大體可以看明白,說是TreadLocal能夠給咱們提供一個線程內的局部變量,並且這個變量與通常的變量還不一樣,它是每一個線程獨有的,與其餘線程互不干擾的。函數

如今咱們簡單的對ThreadLocal有了認識,下面咱們就直接上代碼,看看它的一個實際應用例子。學習

二、如何使用ThreadLocal?

看代碼

先來看一段代碼:測試

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
public class Test {複製代碼 private static int a = 10;複製代碼 private static ThreadLocal<Integer> local;複製代碼 public static void main(String[] args) {複製代碼
複製代碼 Thread A = new Thread(new ThreadA());複製代碼 A.start();複製代碼 ThreadB B = new ThreadB();複製代碼 B.start();複製代碼
複製代碼 }複製代碼
複製代碼 static class ThreadA implements Runnable{複製代碼 @Override複製代碼 public void run() {複製代碼 local = new ThreadLocal();複製代碼 local.set(a+10);複製代碼 System.out.println(local.get()+Thread.currentThread().getName());複製代碼 local.remove();複製代碼 System.out.println(local.get()+Thread.currentThread().getName());複製代碼 }複製代碼 }複製代碼
複製代碼 static class ThreadB extends Thread{複製代碼 @Override複製代碼 public void run() {複製代碼 System.out.println(local.get()+Thread.currentThread().getName());複製代碼
複製代碼 }複製代碼 }複製代碼}複製代碼

咱們以前就知道,ThreadLocal是爲咱們提供一個線程局部變量的,那咱們測試的方法就是建立兩個線程,使用ThreadLocal去存取值,看看兩個線程之間會不會互相影響,上面的這段代碼咱們來簡單分析一下,首先是兩個變量:

  • ounter(line
  • ounter(line
private static int a = 10;複製代碼 private static ThreadLocal<Integer> local;複製代碼

注意看,這裏就使用到了ThreadLocal了,使用方法和普通的變量幾乎是同樣的,咱們這個時候就能夠把ThreadLocal按照一個變量來理解,咱們日常定義一個變量不就是這樣:

  • ounter(line
int a = 10;複製代碼

因此對於ThreadLocal也是同樣,咱們建立一個ThreadLocal就如同新建立一個變量同樣:

  • ounter(line
 private static ThreadLocal<Integer> local;複製代碼

這個時候咱們就定義了一個ThreadLocal,注意這個時候只是定義而沒有進行初始化賦值,並不像int a = 10那樣已經賦值爲10了,如今的ThreadLocal還只是定義好而已,咱們繼續看下面的代碼:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
static class ThreadA implements Runnable{複製代碼 @Override複製代碼 public void run() {複製代碼 local = new ThreadLocal();複製代碼 local.set(a+10);複製代碼 System.out.println(local.get()+Thread.currentThread().getName());複製代碼 local.remove();複製代碼 System.out.println(local.get()+Thread.currentThread().getName());複製代碼 }複製代碼 }複製代碼
複製代碼 static class ThreadB extends Thread{複製代碼 @Override複製代碼 public void run() {複製代碼 System.out.println(local.get()+Thread.currentThread().getName());複製代碼
複製代碼 }複製代碼 }複製代碼

這裏是定義了兩個線程,注意看了,在第一個線程中的run方法內,咱們對ThreadLocal進行了實例化:

  • ounter(line
local = new ThreadLocal();複製代碼

到這裏,咱們就完整的建立了一個ThreadLocal,也就是下面這樣:

  • ounter(line
ThreadLocal local = new ThreadLocal();複製代碼

咱們以前說能夠把ThreadLocal看作是一個變量,像普通的變量,好比下面這樣:

  • ounter(line
int a = 10;複製代碼

就這樣,咱們就給a賦值爲10了,那麼對於ThreadLocal而言,咱們該怎麼給它設置值呢?有以下的操做:

  • ounter(line
  • ounter(line
local.set();複製代碼
複製代碼

就像咱們上面代碼那樣:

  • ounter(line
  • ounter(line
local.set(a+10);複製代碼
複製代碼

這樣咱們就給ThreadLocal給賦值了,那麼怎麼拿到這個值呢?如同上面代碼所示:

  • ounter(line
  • ounter(line
System.out.println(local.get()+Thread.currentThread().getName());複製代碼
複製代碼

也就是經過:

  • ounter(line
  • ounter(line
local.get()複製代碼
複製代碼

至此,咱們就知道ThreadLocal最基本的使用了。

基本使用

也就是:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
ThreadLocal local = new ThreadLocal();複製代碼local.set(a+10);複製代碼local.get()複製代碼
複製代碼

到這裏咱們有沒有以爲它像是一個map,也是key-value的形式來存取值的呢?

另外在上面的代碼中還有以下的一句代碼:

  • ounter(line
  • ounter(line
local.remove();複製代碼
複製代碼

這個也好理解,是刪除,刪除啥呢?咱們先留個疑問,接下來的文章會慢慢說,看到最後,你就明白了。

而後咱們所展現的代碼還有這麼一段:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
Thread A = new Thread(new ThreadA());複製代碼A.start();複製代碼ThreadB B = new ThreadB();複製代碼B.start();複製代碼
複製代碼

這個就是開啓兩個線程。

至此,咱們所展現的代碼就簡單的分析了一下,重點看了ThreadLocal是個簡單的使用。

那麼這段代碼會輸出什麼結果呢?在看輸出以前,咱們須要強調一點,ThreadLocal能夠提供線程內的局部變量,各個線程之間互不干擾。那咱們在思考上面所展現的代碼。首先是定義ThreadLocal:

接下來在第一個線程中實例化而且賦值:

而後咱們看在第二個線程中:

大眼一看,貌似以爲應該仍是20,畢竟是同一個local啊,並且local在以前已經賦值了等於20,這裏只不過在另一個線程中再次去取這個值,咱們來看看輸出結果:

看到結果咱們知道了,雖然在第一個線程中ThreadLocal被實例化且賦值了,並且正確取值20,可是在另外一個線程中去取值的話爲空,咱們再來稍微改變下代碼:

哦,彷佛明白了,對於ThreadLocal而言,每一個線程都是有一個單獨存在的,至關於一個副本,線程之間互不影響,這裏面還有一個null是由於調用了:

  • ounter(line
  • ounter(line
local.remove();複製代碼
複製代碼

這至關於把值刪除了,天然爲空,想想,上述的結果不就說明了ThreadLocal的做用嗎?提供線程局部變量,每一個線程都有本身的一份,線程之間沒有影響。

可能有的人不明白了,這裏的local不都是這個嗎?

難道不是同一個?按理說是一個啊,在另一個線程中應該取值是同樣的啊,怎麼會是空呢?並且在另一個線程中也只是調用了這個簡單的get方法啊:

  • ounter(line
  • ounter(line
local.get()複製代碼
複製代碼

哦,我知道了,這個可能就是get的問題,在不一樣的線程之間get的實現是不一樣的,那它的底層是怎麼實現的呢?

三、ThreadLocal的實現原理

源碼解讀get方法

好了,確定有人火燒眉毛的想看看這個get是怎麼實現的,爲何會出現上述的結果,那咱們就一塊兒來看看這個get的底層源碼:

這個就是get方法的實現了,可能咱們猛一看並不能徹底看明白每一個細節,可是大體意思已經很清楚了,接下來咱們來簡單的分析一下,對了咱們如今要解決的問題是爲何在另外一個線程中調用get方法以後獲得的值是null,也就是這個:

咱們首先來看這兩句代碼:

  • ounter(line
  • ounter(line
  • ounter(line
Thread t = Thread.currentThread();複製代碼ThreadLocalMap map = getMap(t);複製代碼
複製代碼

首先是獲取當前線程,而後根據當前線程獲得一個ThreadLocalMap,這個ThreadLocalMap是個啥,咱們暫時還不知道,解下來就進行了以下判斷:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
if (map != null) {複製代碼 ThreadLocalMap.Entry e = map.getEntry(this);複製代碼 if (e != null) {複製代碼 @SuppressWarnings("unchecked")複製代碼 T result = (T)e.value;複製代碼 return result;複製代碼 }複製代碼 }複製代碼
複製代碼

也就是在判斷根據當前線程獲得的ThreadLocalMap是否爲空,咱們想一想,咱們就是直接調用get就來到了這裏,好像並滅有什麼地方去建立了這個ThreadLocalMap吧,那麼這裏判斷的就是空了,因此就會去走以下的語句:

  • ounter(line
  • ounter(line
return setInitialValue();複製代碼
複製代碼

雖然這裏咱們並無這個Map,可是咱們看若是有map的話是怎麼執行呢?咱們仔細看看這段代碼:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
ThreadLocalMap.Entry e = map.getEntry(this);複製代碼 if (e != null) {複製代碼 @SuppressWarnings("unchecked")複製代碼 T result = (T)e.value;複製代碼 return result;複製代碼 }複製代碼
複製代碼

這不就是在返回咱們須要的值嘛?這個值是從這個ThreadLocalMap中拿到的,哦,到了這裏彷佛知道了,爲啥在另外一個線程中調用get會獲得null,那是由於值被放到了一個叫作ThreadLocalMap的東西里面了,而它又是根據當前線程建立的,也就是說每一個線程的ThreadLocalMap是不一樣的,在當前線程中並無建立,因此也就沒值。

爲何是null?

嗯嗯,這個想法貌似很對,不過又有個問題,爲啥會是null呢?咱們就要看這個語句的執行了:

  • ounter(line
  • ounter(line
return setInitialValue();複製代碼
複製代碼

從這個方法的名字能夠猜測,這應該是初始化操做的。咱們看看這方法是如何實現的:

在這個方法之中,首先會執行以下語句:

  • ounter(line
  • ounter(line
T value = initialValue();複製代碼
複製代碼

咱們看看這個方法的實現:

原來就返回一個null啊,那麼上面的value就是null了,而後咱們再看下面的語句,是否是以爲很熟悉:

咱們知道,這裏map是沒有的,因此會走else,也就是回去執行以下的方法:

  • ounter(line
  • ounter(line
createMap(t, value);複製代碼
複製代碼

對了,這裏的value是個null,而t就是當前線程啦,咱們繼續看看這個方法的實現:

哦,看到這裏彷佛就知道,在這個方法中就建立了一個ThreadLocalMap,咱們以前看源碼以爲數據是被放到了這個ThreadLocalMap中了,那麼如今這裏已經建立了一個ThreadLocalMap,那麼數據是哪一個呢?看方法實現,應該就是那個firstValue了,咱們知道這個值就是以前傳過來的value,也就是null,這至關於一個value值,那麼這裏的key呢?是否是就是這個this,那麼這個this指的誰呢?

這裏的this實際上是ThreadLocal的實例,也就是以前的local:

因此到了如今,這個get方法的咱們分析的結果就是建立了一個ThreadLocalMap,而後往裏面放了值,是一個key-value的形式,key就是咱們的ThreadLocal實例。

而後咱們再看執行完createMap以後的操做,就是直接返回value了,也就是一個null,因此如今咱們明白了爲何這裏調用get是個null。

看到這裏,這個get是明白怎麼回事了,那麼在第一個線程中的get也是這樣的嗎?

源碼解讀set方法

對於get的方法實現確定是同樣的,之因此這裏獲得值20,那是由於在當前線程執行了set方法:

  • ounter(line
  • ounter(line
local.set(a+10);複製代碼
複製代碼

根據咱們以前對get的分析,這裏咱們應該能夠猜測到,set方法也建立了一個ThreadLocalMap而且把值放了進去,因此執行get能獲得值,咱們一塊兒來看下set的實現:

是否是很熟悉,也是先拿到當前線程,而後根據當前線程獲得ThreadLocalMap,這裏一樣以前沒有,因此須要從新建立,也就是去執行:

  • ounter(line
  • ounter(line
createMap(t, value);複製代碼
複製代碼

可是這裏的value就不是null了,而是傳過來的20,咱們接着看這個方法的實現:

熟悉不,又到了這裏,建立了一個新的ThreadLocalMap來存放數據,this一樣也是ThreadLocal的實例,也就是local,這樣一來,key就對應咱們的ThreadLocal實例,value就是傳過來的20了,另外咱們大概知道,這麼個鍵值對是放在ThreadLocalMap中的,而後咱們經過當前線程能夠獲得這個ThreadLocalMap,再根據ThreadLocal這個實例就能夠獲得value的值,也就是20.

咱們接下來看這個線程中的get的執行:

由於咱們在set的時候就建立了ThreadLocalMap,因此這裏就不會再去建立了,由於已經有map了,因此會直接執行:

ThreadLocalMap的源碼解讀

這裏其實就牽涉到ThreadLocalMap的內部實現了,看到這裏咱們須要來藉助一張圖以便加深理解,就是下面的這張圖:

通過咱們上面的分析,咱們知道ThreadLocal的設置值的方式是key-value的形式,也知道了這裏的key其實就是ThreadLocal的實例,value就是要設置的值。

這裏咱們看下ThreadLocalMap,它實際上是一個數據結構,就是用來存放咱們的值的,並且它也是ThreadLocal的一個核心,咱們經過上面這張圖,首先要知道的一點就是:

ThreadLocalMap中存儲的是Entry對象,Entry對象中存放的是key和value。

至於爲何是這樣的,咱們一步步的來分析ThreadLocalMap!

ThreadLocalMap中的Entry

在ThreadLocalMap中實際上是維護了一張哈希表,這個表裏面就是Entry對象,而每個Entry對象簡單來講就是存放了咱們的key和value值。

那麼這個是如何實現的呢?首先咱們來想,Entry對象是存放在ThreadLocalMap中,那麼對於TreadLocalMap而言就須要一個什麼來存放這個Entry對象,咱們能夠想成一個容器,也就是說ThreadLocalMap須要有一個容器來存放Entry對象,咱們來看ThreadLocalMap的源碼實現:

在ThreadLocalMap中定義了一個Entry數組table,這個就是存放Entry的一個容器,在這裏咱們首先須要知道一個概念,那就是什麼是哈希表?

百度百科是這樣解釋的:

散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表

給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能獲得包含該關鍵字的記錄在表中的地址,則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數。

上面也提到過,ThreadLocalMap其實就是維護了一張哈希表,也便是一個數組,這個表裏面存儲的就是咱們的Entry對象,其實就是它:

哈希表擴容

涉及到哈希表,必然會涉及到另一個概念,那就是增加因子,那什麼是增加因子呢?

簡單來講,這是一個值,當表裏面存儲的對象達到了表的總容量的某個百分比的時候,這張表就該擴容了,那麼這個百分比就是增加因子,咱們看ThreadLocalMap中的增加因子:

從這些代碼咱們能夠了解到,ThreadLocalMap中定義了一個threshold屬性,這個屬性上面有個介紹,也就是:

The next size value at which to resize.

翻譯過來就是:要調整大小的下一個大小值。

什麼意思呢?也就是說當哈希表中存儲的對象的數量超過了這個值的時候,哈希表就須要擴容,那麼這個值具體是多大呢?下面有個方法:

它也有個註釋:

Set the resize threshold to maintain at worst a 2/3 load factor.

翻譯過來就是:設置調整大小閾值以保持最壞的2/3負載係數。

意思就是設定這個增加因子爲總容量的2/3,這個增加因子就是threshold。也就是當哈希表的容量達到了總容量的2/3的時候就須要對哈希表進行擴容了。

Entry對象是如何存儲數據的

到這裏咱們就知道了,ThreadLocalMap維護了一個哈希表,表裏面存儲的就是Entry對象,當哈希表的當前容量達到了總容量的2/3的時候就須要對哈希表進行擴容了。

那麼可能有人會問了,初始容量是多少啊?這個在源碼中也有展示:

也便是16,那麼對於數據而言,它又是怎樣被放到哈希表中的呢?接下來咱們就來看看ThreadLocalMap中設置值的方法:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
private void set(ThreadLocal<?> key, Object value) {複製代碼
複製代碼 // We don't use a fast path as with get() because it is at複製代碼 // least as common to use set() to create new entries as複製代碼 // it is to replace existing ones, in which case, a fast複製代碼 // path would fail more often than not.複製代碼
複製代碼 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)]) {複製代碼 ThreadLocal<?> k = e.get();複製代碼
複製代碼 if (k == key) {複製代碼 e.value = value;複製代碼 return;複製代碼 }複製代碼
複製代碼 if (k == null) {複製代碼 replaceStaleEntry(key, value, i);複製代碼 return;複製代碼 }複製代碼 }複製代碼
複製代碼 tab[i] = new Entry(key, value);複製代碼 int sz = ++size;複製代碼 if (!cleanSomeSlots(i, sz) && sz >= threshold)複製代碼 rehash();複製代碼 }複製代碼
複製代碼

咱們來一步步的分析這段源碼,看看數據是如何被存儲的,爲了讓你們更加的明白,咱們仍是從最開始的ThreadLocal設置值得時候開始一步步的進入到這段源代碼,首先就是這段代碼:

這是在第一個線程中,咱們對ThreadLocal進行了實例化,而且在第一個線程總開始設置值,也就是調用set方法,咱們進入到這個set方法看看:

咱們以前就分析過了,這裏沒有map,會去建立,咱們進入到createMap中看看:

這裏建立了ThredLocalMap,調用了它的構造方法,咱們進入看看:

這段代碼就須要好好解讀了,首先是它:

  • ounter(line
  • ounter(line
table = new Entry[INITIAL_CAPACITY];複製代碼
複製代碼

這個table沒有忘記是啥吧,就是以前定義的Entry數組,就是這個:

這裏的INITIAL_CAPACITY就是初始化容量16,因此這裏就構建了一個容量爲16的Entry數組,這個數組就能夠用來存放咱們的數據,具體怎麼存放,咱們接着往下看:

  • ounter(line
  • ounter(line
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);複製代碼
複製代碼

這裏是爲了獲得一個下表,由於哈希表是依靠一個索引去存取值得,因此會根據這個下標值去決定把數據存放到哪一個位置,簡單點就是把數據放到數組中的哪一個位置,這個就是數組下標,那這個threadLocalHashCode是個啥呢?咱們看看:

它是經過這個nextHashCode方法獲得的,這個nextHashCode也有一系列的操做,反正最終目的就是爲了獲得一個索引值,或者是下標值,來決定這個數據存放到哪一個位置。

那爲何這樣寫呢?

  • ounter(line
  • ounter(line
firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);複製代碼
複製代碼

這是拿獲得的threadLocalHashCode對Entry數組的總容量減去一的值作取餘操做,目的就是爲了獲得的下標值始終都在數組內,防止下標越界的。

再接着看剩下的代碼:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
table[i] = new Entry(firstKey, firstValue);複製代碼 size = 1;複製代碼 setThreshold(INITIAL_CAPACITY);複製代碼
複製代碼

拿到下標值以後就獲得了一個位置就是table[i],而後就是把一個具體的Entry對象放進去了,剩下的就是設置當前表中有幾條數據,也就是有幾個Entry對象了,而後根據初始容量設置增加因子,咱們重點來看看這段代碼:

  • ounter(line
  • ounter(line
table[i] = new Entry(firstKey, firstValue);複製代碼
複製代碼

table[i]也就是Entry數組中的一個確切的位置,是要放入一個Entry對象的,這裏就new了一個新的Entry對象,並把key和value傳入了進去,咱們看看這個Entry的構造方法以及這個Entry類的實現。

Entry長啥樣?

咱們先來看看它的這個構造函數:

這其實也是Entry類的源碼,其中有一個構造函數,傳入key和value,在Entry中還定義了一個Object類型的value變量,把隨構造函數傳入進來的value值賦值給這個Object類型的value變量,這樣就將value保存在了Entry中了。

咱們再來看這個Entry的實現,它是繼承了WeakReference<ThreadLocal>,這個是啥?WeakReference>是一個弱引用類型,簡單說,Entry本質上就是一個弱引用,由於是繼承WeakReference<ThreadLocal<?>>這個弱引用,因此它其實也是個弱引用,而Entry的實例說白了就是對ThreadLocal實例的一個弱引用,只不過Entry的設計上同時還保存了value值。

到這裏,咱們就知道了這個Entry是如何保存鍵值對的了,也知道Entry其實就是個弱引用。

對了,你要知道上述咱們的分析是針對ThreadLocal第一次調用set方法的時候由於沒有map須要建立map走得上述方法,若是是再次調用則會走map中的set方法,咱們具體看源碼:

因爲咱們在第一次調用set方法時已經建立了map,那麼再次set的時候就會主席那個map的set方法,咱們來看看map的set方法是如何實現的:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
private void set(ThreadLocal<?> key, Object value) {複製代碼
複製代碼 // We don't use a fast path as with get() because it is at複製代碼 // least as common to use set() to create new entries as複製代碼 // it is to replace existing ones, in which case, a fast複製代碼 // path would fail more often than not.複製代碼
複製代碼 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)]) {複製代碼 ThreadLocal<?> k = e.get();複製代碼
複製代碼 if (k == key) {複製代碼 e.value = value;複製代碼 return;複製代碼 }複製代碼
複製代碼 if (k == null) {複製代碼 replaceStaleEntry(key, value, i);複製代碼 return;複製代碼 }複製代碼 }複製代碼
複製代碼 tab[i] = new Entry(key, value);複製代碼 int sz = ++size;複製代碼 if (!cleanSomeSlots(i, sz) && sz >= threshold)複製代碼 rehash();複製代碼 }複製代碼
複製代碼

這就是ThreadLocalMap中經過set方式設置值的源碼實現,第一次調用是經過構造函數的形式設置數據,咱們如今來看看這個set方式的數據設置。

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
Entry[] tab = table;複製代碼 int len = tab.length;複製代碼 int i = key.threadLocalHashCode & (len-1);複製代碼
複製代碼

首先是拿到以前建立的Entry數組,這裏是tab,而後也是計算出一個新的下標值來存放新數據,接下來就是這段代碼:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
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) {複製代碼 replaceStaleEntry(key, value, i);複製代碼 return;複製代碼 }複製代碼 }複製代碼
複製代碼

首先要知道這是一個for循環,根據一個下標值獲得一個新的Entry對象,而後進入循環條件 也便是這個Entry對象不爲null,而後執行循環體,循環體中有兩個判斷,還有一個根據當前Entry對象獲得ThreadLocal的引用,也便是Key,不過這裏定義爲k。

如今咱們要知道,咱們是要往Entry數組中放入一個新的Entry對象,具體放到哪裏由i這個下標值肯定,具體的位置就是table[i],因此會出現的狀況就有這個位置本來就有一個Entry對象或者爲null,因而若是本來就有的話並且引用的是同一個ThreadLocal的話,那麼就把值給覆蓋掉:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
if (k == key) {複製代碼 e.value = value;複製代碼 return;複製代碼 }複製代碼
複製代碼

若是是這個位置是null的話,咱們就放入新的值:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
if (k == null) {複製代碼 replaceStaleEntry(key, value, i);複製代碼 return;複製代碼 }複製代碼
複製代碼

固然,也會出現的狀況就是這個位置不爲null,並且也不是同一個ThreadLocal的引用,那麼就須要繼續日後挪一個位置來放入新的數據:

  • ounter(line
  • ounter(line
e = tab[i = nextIndex(i, len)])複製代碼
複製代碼

固然,這個新的位置上依然要進入判斷,也是上面的狀況,以此類推,直到找到一個位置要麼爲null,要麼是同一個ThreadLocal的引用,只有這樣才能放入新的數據。

咱們接着來看下面的代碼,執行完上面的判斷以後會執行以下的代碼:

  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
  • ounter(line
 tab[i] = new Entry(key, value);複製代碼 int sz = ++size;複製代碼 if (!cleanSomeSlots(i, sz) && sz >= threshold)複製代碼 rehash();複製代碼
複製代碼

這個就是建立具體的Entry對象,由於Entry數組多了一個Entry對象,因此總條目須要加一,而這個if判斷則是爲了看看當前存儲的對象個數是否達到了增加因子,也就是判斷下是否須要擴容,若是須要擴容了該怎麼辦呢?這個時候要依靠的就是這個rehash函數了。

rehash函數是如何實現從新擴充並從新計算位置的

若是達到了增加因子,那就須要從新擴充,並且還須要將全部的對象從新計算位置,咱們來看rehash函數的實現:

咱們看到在if判斷中判斷的指標是增加因子的3/4,這是怎麼回事,以前不是說增加因子是2/3嘛?超過這個值才須要擴容,這怎麼變成了增加因子的3/4纔開始擴容呢?咱們以前說過,ThreadLocalMap中存儲的是Entry對象,Entry本質上是個ThreadLocal的弱引用,因此它隨時都有可能被回收掉,這樣就會出現key值爲null的Entry對象,這些都是用不到的,須要刪除掉來騰出空間,這樣一來,實際上存儲的對象個數就減小了,因此後面的判斷就是增加因子的3/4,而不是增加因子2/3了。

而expungeStaleEntries();就是作這樣的清理工做的,把佔坑的Entry通通刪除掉。

如何獲取Entry對象中的數據

那該如何獲取到Entry對象中的數據呢?也便是咱們使用ThreadLocal的實例去調用get方法取值:

由於已經有map了,因此咱們直接就調用map的getEntry方法,咱們看看這個方法的實現:

這段代碼仍是比較簡單的,首先根據哈希碼值算出下標i,而後就肯定了這個Entry的位置,若是這個位置不爲空並且對用的ThreadLocal的弱引用也是咱們須要的這個,那麼就返回這個Entry對象中保存的value值。

固然,若是對應的位置爲空的話,咱們就須要getEntryAfterMiss函數來進行進一步的判斷了。

到了這裏相信你們對ThreadLocalMap就有了必定的認識了,接下來咱們繼續來聊聊ThreadLocal的內存泄露問題。

四、ThreadLocal的內存泄露

什麼是內存泄漏和內存溢出

咱們在講ThreadLocal的內存泄漏以前,首先要搞清楚什麼是內存泄漏,那要提及內存泄漏,確定還有個概念須要說,那就是內存溢出,這二者是個啥呢?

首先什麼是內存泄漏:

說的簡單點那就是由於操做不當或者一些錯誤致使沒有能釋放掉已經再也不使用的內存,這就是內存泄漏,也就是說,有些內存已經不會再使用了,可是卻沒有給它釋放掉,這就一直佔用着內存空間,從而致使了內存泄漏。

那什麼是內存溢出呢?

這個簡單點說就是內存不夠用了,我運行一個程序好比說須要50M的內存,可是如今內存就剩下20M了,那程序運行就會發生內存溢出,也就是告訴你內存不夠用,這時候程序就沒法運行了。

好,瞭解了基本概念以後,咱們再來看看T和read Local的內存泄漏,那爲何T和read Local會產生內存泄漏呢?咱們再來看看這張圖:

通過咱們上述的討論,咱們大體知道了ThreadLocal其實本質上是在每一個線程中單獨維護了一個ThreadLocalMap數據結構,這個ThreadLocalMap是每一個線程獨有的,只有根據當前線程才能找到當前線程的這個ThreadLocalMap,這就實現了線程以前的隔離。

咱們看上面那張圖,每一個線程根據找到本身維護的ThreadLocalMap,而後能夠操做這個數據結構,往裏面存取數據,而ThreadLocalMap中維護的就是一個Entry數組,每一個Entry對象就是咱們存放的數據,它是個key-value的形式,key就是ThreadLocal實例的弱引用,value就是咱們要存放的數據,也就是一個ThreadLocal的實例會對用一個數據,造成一個鍵值對。

若是有兩個線程,持有同一個ThreaLocal的實例,這樣的狀況也就是Entry對象持有的ThreadLocal的弱引用是同樣的,可是兩個線程的ThreadLocalMap是不一樣的,記住一點,那就是ThreadLocalMap是每一個線程單獨維護的。

爲何會出現內存泄漏

那咱們如今來看,爲何ThreadLocal會出現內存泄漏,咱們以前也說過了,Entry對象持有的是鍵就是ThreadLocal實例的弱引用,弱引用有個什麼特色呢?那就是在垃圾回收的時候會被回收掉,能夠根據上圖想一下,圖中虛線就表明弱引用,若是這個ThreadLocal實例被回收掉,這個弱引用的連接也就斷開了,就像這樣:

那麼這樣在Entry對象中的key就變成了null,因此這個Entry對象就沒有被引用,由於key變成看null,就取不到這個value值了,再加上若是這個當前線程遲遲沒有結束,ThreadLocalMap的生命週期就跟線程同樣,這樣就會存在一個強引用鏈,因此這個時候,key爲null的這個Entry就形成了內存泄漏。

由於它沒有用了,可是尚未被釋放。

如何解決內存泄漏

明白瞭如何產生的內存泄漏,也就知道了怎麼解決,通過上面的分析,咱們大體知道了在ThreadLocalMap中存在key爲null的Entry對象,從而致使內存泄漏,那麼只要把這些Entry都給刪除掉,也就解決了內存泄漏。

咱們每次使用ThreadLocal就會隨線程產生一個ThreadLocalMap,裏面維護Entry對象,咱們對Entry進行存取值,那麼若是咱們每次使用完ThreadLocal以後就把對應的Entry給刪除掉,這樣不就解決了內粗泄漏嘛,那怎麼作呢?

在ThreadLocal中提供了一個remove方法:

這個就是根據key刪除掉對應的Entry,如此一來,咱們就解決了內存泄漏問題,由於可能出現內存泄漏的Entry,在咱們使用完以後就立馬刪除了。

因此對於ThreadLocal而言,就應該像使用鎖同樣,加鎖以後要記得解鎖,也就是調用它的remove方法,用完就清理。

五、總結

至此,咱們已經對ThreadLocal作了一個較爲全面和深刻的分析,你們應該也對它有了更深的印象,下面針對本文來作一個簡單的總結:

一、ThreadLocal是用來提供線程局部變量的,在線程內能夠隨時隨地的存取數據,並且線程之間是互不干擾的。

二、ThreadLocal其實是在每一個線程內部維護了一個ThreadLocalMap,這個ThreadLocalMap是每一個線程獨有的,裏面存儲的是Entry對象,Entry對象其實是個ThreadLocal的實例的弱引用,同時還保存了value值,也就是說Entry存儲的是鍵值對的形式的值,key就是ThreadLocal實例自己,value則是要存儲的數據。

三、TreadLocal的核心是底層維護的ThreadLocalMap,它的底層是一個自定義的哈希表,增加因子是2/3,增加因子也能夠叫作是一個閾值,底層定義爲threshold,當哈希表容量大於或等於閾值的3/4的時候就開始擴容底層的哈希表數組table。

四、ThreaLocalMap中存儲的核心元素是Entry,Entry是一個弱引用,因此在垃圾回收的時候,ThreadLocal若是沒有外部的強引用,它會被回收掉,這樣就會產生key爲null的Entry了,這樣也就產生了內存泄漏。

五、在ThreadLocal的get(),set()和remove()的時候都會清除ThreadLocalMap中key爲null的Entry,若是咱們不手動清除,就會形成內存泄漏,最佳作法是使用ThreadLocal就像使用鎖同樣,加鎖以後要解鎖,也就是用完就使用remove進行清理。

六、關於原創

本文原創做者:ithuangqing 轉載請註明出處,微信公衆號開白請聯繫我微信H653836923

▼ 慶哥有一個夢想,寫一些能讓小白看得懂學得會的技術教程,幫助初學者更快的入門與進階,因而乎,在編碼以外開啓了逐夢之旅!關注公衆號,後臺回覆「慶哥」,2019最新java自學資源立馬送上!

在這裏插入圖片描述
在這裏插入圖片描述

長按二維碼識別關注!

相關文章
相關標籤/搜索