1. 程式人生 > >ThreadLocal之深度解讀

ThreadLocal之深度解讀

微信公眾號:I am CR7
如有問題或建議,請在下方留言;
最近更新:2019-01-12

前言

繼上一篇文章《Spring Cloud Netflix Zuul原始碼分析之請求處理篇》中提到的RequestContext使用的兩大神器之一:ThreadLocal,本文特此進行深入分析,為大家掃清知識障礙。

Hello World

在展開深入分析之前,咱們先來看一個官方示例:

出處來源於ThreadLocal類上的註釋,其中main方法是筆者加上的。

 1import java.util.concurrent.atomic.AtomicInteger;
2

3public class ThreadId {
4    // Atomic integer containing the next thread ID to be assigned
5    private static final AtomicInteger nextId = new AtomicInteger(0);
6

7    // Thread local variable containing each thread's ID
8    private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
9        @Override

10        protected Integer initialValue() {
11            return nextId.getAndIncrement();
12        }
13    };
14
15    // Returns the current thread's unique ID, assigning it if necessary
16    public static int get() {
17        return threadId.get();
18    }
19
20    public static void main(String[] args) {
21        for (int i = 0; i < 5; i++) {
22            new Thread(new Runnable() {
23                @Override
24                public void run() {
25                    System.out.println("threadName=" + Thread.currentThread().getName() + ",threadId=" + ThreadId.get());
26                }
27            }).start();
28        }
29    }
30}
複製程式碼

執行結果如下:

1threadName=Thread-0,threadId=0
2threadName=Thread-1,threadId=1
3threadName=Thread-2,threadId=2
4threadName=Thread-3,threadId=3
5threadName=Thread-4,threadId=4
複製程式碼

我問:看完這個例子,您知道ThreadLocal是幹什麼的了嗎?
您答:不知道,沒感覺,一個hello world的例子,完全激發不了我的興趣。
您問:那個誰,你敢不敢舉一個生產級的、工作中真實能用的例子?
我答:得,您是"爺",您說啥我就做啥。還記得《Spring Cloud Netflix Zuul原始碼分析之請求處理篇》中提到的RequestContext嗎?這就是一個生產級的運用啊。Zuul核心原理是什麼?就是將請求放入過濾器鏈中經過一個個過濾器的處理,過濾器之間沒有直接的呼叫關係,處理的結果都是存放在RequestContext裡傳遞的,而這個RequestContext就是一個ThreadLocal型別的物件啊!!!

 1public class RequestContext extends ConcurrentHashMap<StringObject{
2
3    protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
4        @Override
5        protected RequestContext initialValue() {
6            try {
7                return contextClass.newInstance();
8            } catch (Throwable e) {
9                throw new RuntimeException(e);
10            }
11        }
12    };
13
14    public static RequestContext getCurrentContext() {
15        if (testContext != nullreturn testContext;
16
17        RequestContext context = threadLocal.get();
18        return context;
19    }
20}
複製程式碼

以Zuul中前置過濾器DebugFilter為例:

 1public class DebugFilter extends ZuulFilter {
2
3    @Override
4    public Object run() {
5        // 獲取ThreadLocal物件RequestContext
6        RequestContext ctx = RequestContext.getCurrentContext();
7        // 它是一個map,可以放入資料,給後面的過濾器使用
8        ctx.setDebugRouting(true);
9        ctx.setDebugRequest(true);
10        return null;
11    }
12
13}
複製程式碼

您問:那說了半天,它到底是什麼,有什麼用,能不能給個概念?
我答:能!必須能!!!

What is this

它是啥?它是一個支援泛型的java類啊,拋開裡面的靜態內部類ThreadLocalMap不說,其實它沒幾行程式碼,不信,您自己去看看。它用來幹啥?類上註釋說的很明白:

  • 它能讓執行緒擁有了自己內部獨享的變數
  • 每一個執行緒可以通過get、set方法去進行操作
  • 可以覆蓋initialValue方法指定執行緒獨享的值
  • 通常會用來修飾類裡private static final的屬性,為執行緒設定一些狀態資訊,例如user ID或者Transaction ID
  • 每一個執行緒都有一個指向threadLocal例項的弱引用,只要執行緒一直存活或者該threadLocal例項能被訪問到,都不會被垃圾回收清理掉

愛提問的您,一定會有疑惑,demo裡只是呼叫了ThreadLocal.get()方法,它如何實現這偉大的一切呢?這就是筆者下面要講的內容,走著~~~

我有我的map

話不多說,我們來看get方法內部實現:

get()原始碼
 1public T get() {
2    Thread t = Thread.currentThread();
3    ThreadLocalMap map = getMap(t);
4    if (map != null) {
5        ThreadLocalMap.Entry e = map.getEntry(this);
6        if (e != null) {
7            @SuppressWarnings("unchecked")
8            T result = (T)e.value;
9            return result;
10        }
11    }
12    return setInitialValue();
13}
複製程式碼

邏輯很簡單:

  • 獲取當前執行緒內部的ThreadLocalMap
  • map存在則獲取當前ThreadLocal對應的value值
  • map不存在或者找不到value值,則呼叫setInitialValue,進行初始化
setInitialValue()原始碼
 1private T setInitialValue({
2    T value = initialValue();
3    Thread t = Thread.currentThread();
4    ThreadLocalMap map = getMap(t);
5    if (map != null)
6        map.set(thisvalue);
7    else
8        createMap(t, value);
9    return value;
10}
複製程式碼

邏輯也很簡單:

  • 呼叫initialValue方法,獲取初始化值【呼叫者通過覆蓋該方法,設定自己的初始化值】
  • 獲取當前執行緒內部的ThreadLocalMap
  • map存在則把當前ThreadLocal和value新增到map中
  • map不存在則建立一個ThreadLocalMap,儲存到當前執行緒內部
時序圖

為了便於理解,筆者特地畫了一個時序圖,請看:

get方法時序圖
get方法時序圖
小結

至此,您能回答ThreadLocal的實現原理了嗎?沒錯,map,一個叫做ThreadLocalMap的map,這是關鍵。每一個執行緒都有一個私有變數,是ThreadLocalMap型別。當為執行緒新增ThreadLocal物件時,就是儲存到這個map中,所以執行緒與執行緒間不會互相干擾。總結起來,一句話:我有我的young,哦,不對,是我有我的map。弄清楚了這些,是不是使用的時候就自信了很多。但是,這是不是就意味著可以大膽的去使用了呢?其實,不盡然,有一個“大坑”在等著你。

神奇的remove

那個“大坑”指的就是因為ThreadLocal使用不當,會引發記憶體洩露的問題。筆者給出兩段示例程式碼,來說明這個問題。

程式碼出處來源於Stack Overflow:stackoverflow.com/questions/1…

示例一:
 1public class MemoryLeak {
2
3    public static void main(String[] args{
4        new Thread(new Runnable() {
5            @Override
6            public void run(
{
7                for (int i = 0; i < 1000; i++) {
8                    TestClass t = new TestClass(i);
9                    t.printId();
10                    t = null;
11                }
12            }
13        }).start();
14    }
15
16    static class TestClass{
17        private int id;
18        private int[] arr;
19        private ThreadLocal<TestClass> threadLocal;
20        TestClass(int id){
21            this.id = id;
22            arr = new int[1000000];
23            threadLocal = new ThreadLocal<>();
24            threadLocal.set(this);
25        }
26
27        public void printId(){
28            System.out.println(threadLocal.get().id);
29        }
30    }
31}
複製程式碼

執行結果:

 10
21
32
43
5...省略...
6440
7441
8442
9443
10444
11Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
12    at com.gentlemanqc.MemoryLeak$TestClass.<init>(MemoryLeak.java:33)
13    at com.gentlemanqc.MemoryLeak$1.run(MemoryLeak.java:16)
14    at java.lang.Thread.run(Thread.java:745)
複製程式碼

對上述程式碼稍作修改,請看:

 1public class MemoryLeak {
2
3    public static void main(String[] args{
4        new Thread(new Runnable() {
5            @Override
6            public void run(
{
7                for (int i = 0; i < 1000; i++) {
8                    TestClass t = new TestClass(i);
9                    t.printId();
10                    t.threadLocal.remove();
11                }
12            }
13        }).start();
14    }
15
16    static class TestClass{
17        private int id;
18        private int[] arr;
19        private ThreadLocal<TestClass> threadLocal;
20        TestClass(int id){
21            this.id = id;
22            arr = new int[1000000];
23            threadLocal = new ThreadLocal<>();
24            threadLocal.set(this);
25        }
26
27        public void printId(){
28            System.out.println(threadLocal.get().id);
29        }
30    }
31}
複製程式碼

執行結果:

10
21
32
43
5...省略...
6996
7997
8998
9999
複製程式碼

一個記憶體洩漏,一個正常完成,對比程式碼只有一處不同:t = null改為了t.threadLocal.remove(); 哇,神奇的remove!!!筆者先留個懸念,暫且不去分析原因。我們先來看看上述示例中涉及到的兩個方法:set()和remove()。

set(T value)原始碼
1public void set(value{
2    Thread t = Thread.currentThread();
3    ThreadLocalMap map = getMap(t);
4    if (map != null)
5        map.set(thisvalue);
6    else
7        createMap(t, value);
8}
複製程式碼

邏輯很簡單:

  • 獲取當前執行緒內部的ThreadLocalMap
  • map存在則把當前ThreadLocal和value新增到map中
  • map不存在則建立一個ThreadLocalMap,儲存到當前執行緒內部
remove原始碼
1public void remove({
2    ThreadLocalMap m = getMap(Thread.currentThread());
3    if (m != null)
4     m.remove(this);
5}
複製程式碼

就一句話,獲取當前執行緒內部的ThreadLocalMap,存在則從map中刪除這個ThreadLocal物件。

小結

講到這裡,ThreadLocal最常用的四種方法都已經說完了,細心的您是不是已經發現,每一個方法都離不開一個類,那就是ThreadLocalMap。所以,要更好的理解ThreadLocal,就有必要深入的去學習這個map。

無處不在的ThreadLocalMap

還是老規矩,先來看看類上的註釋,翻譯過來就是這麼幾點:

  • ThreadLocalMap是一個自定義的hash map,專門用來儲存執行緒的thread local變數
  • 它的操作僅限於ThreadLocal類中,不對外暴露
  • 這個類被用在Thread類的私有變數threadLocals和inheritableThreadLocals上
  • 為了能夠儲存大量且存活時間較長的threadLocal例項,hash table entries採用了WeakReferences作為key的型別
  • 一旦hash table執行空間不足時,key為null的entry就會被清理掉

我們來看下類的宣告資訊:

 1static class ThreadLocalMap {
2
3    // hash map中的entry繼承自弱引用WeakReference,指向threadLocal物件
4    // 對於key為null的entry,說明不再需要訪問,會從table表中清理掉
5    // 這種entry被成為“stale entries”
6    static class Entry extends WeakReference<ThreadLocal<?>> {
7        /** The value associated with this ThreadLocal. */
8        Object value;
9
10        Entry(ThreadLocal<?> k, Object v) {
11            super(k);
12            value = v;
13        }
14    }
15
16    /**
17     * The initial capacity -- MUST be a power of two.
18     */

19    private static final int INITIAL_CAPACITY = 16;
20
21    /**
22     * The table, resized as necessary.
23     * table.length MUST always be a power of two.
24     */

25    private Entry[] table;
26
27    /**
28     * The number of entries in the table.
29     */

30    private int size = 0;
31
32    /**
33     * The next size value at which to resize.
34     */

35    private int threshold; // Default to 0
36
37    /**
38     * Set the resize threshold to maintain at worst a 2/3 load factor.
39     */

40    private void setThreshold(int len) {
41        threshold = len * 2 / 3;
42    }
43
44    /**
45     * Construct a new map initially containing (firstKey, firstValue).
46     * ThreadLocalMaps are constructed lazily, so we only create
47     * one when we have at least one entry to put in it.
48     */

49    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
50        table = new Entry[INITIAL_CAPACITY];
51        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
52        table[i] = new Entry(firstKey, firstValue);
53        size = 1;
54        setThreshold(INITIAL_CAPACITY);
55    }
56}
複製程式碼

當建立一個ThreadLocalMap時,實際上內部是構建了一個Entry型別的陣列,初始化大小為16,閾值threshold為陣列長度的2/3,Entry型別為WeakReference,有一個弱引用指向ThreadLocal物件。

為什麼Entry採用WeakReference型別?

Java垃圾回收時,看一個物件需不需要回收,就是看這個物件是否可達。什麼是可達,就是能不能通過引用去訪問到這個物件。(當然,垃圾回收的策略遠比這個複雜,這裡為了便於理解,簡單給大家說一下)。

jdk1.2以後,引用就被分為四種類型:強引用、弱引用、軟引用和虛引用。強引用就是我們常用的Object obj = new Object(),obj就是一個強引用,指向了物件記憶體空間。當記憶體空間不足時,Java垃圾回收程式發現物件有一個強引用,寧願丟擲OutofMemory錯誤,也不會去回收一個強引用的記憶體空間。而弱引用,即WeakReference,意思就是當一個物件只有弱引用指向它時,垃圾回收器不管當前記憶體是否足夠,都會進行回收。反過來說,這個物件是否要被垃圾回收掉,取決於是否有強引用指向。ThreadLocalMap這麼做,是不想因為自己儲存了ThreadLocal物件,而影響到它的垃圾回收,而是把這個主動權完全交給了呼叫方,一旦呼叫方不想使用,設定ThreadLocal物件為null,記憶體就可以被回收掉。

記憶體溢位問題解答

至此,該做的鋪墊都已經完成了,此時,我們可以來看看上面那個記憶體洩漏的例子。示例中執行一次for迴圈裡的程式碼後,對應的記憶體狀態:

記憶體狀態
記憶體狀態
  • t為建立TestClass物件返回的引用,臨時變數,在一次for迴圈後就執行出棧了
  • thread為建立Thread物件返回的引用,run方法在執行過程中,暫時不會執行出棧

呼叫t=null後,雖然無法再通過t訪問記憶體地址MemoryLeak

 1不能識別此Latex公式:
2[email protected]538,但是當前執行緒依舊存活,可以通過thread指向的記憶體地址,訪問到Thread物件,從而訪問到ThreadLocalMap物件,訪問到value指向的記憶體空間,訪問到arr指向的記憶體空間,從而導致Java垃圾回收並不會回收int[1000000]@541這一片空間。那麼隨著迴圈多次之後,不被回收的堆空間越來越大,最後丟擲java.lang.OutOfMemoryError: Java heap space。
3
4您問:那為什麼呼叫t.threadLocal.remove()就可以呢?
5
6我答:這就得看remove方法裡究竟做了什麼了,請看:
7
image

8是不是恍然大悟?來看下呼叫remove方法之後的記憶體狀態:
9
image

10因為remove方法將referent和value都被設定為null,所以[email protected]540和Memory
複製程式碼
[email protected]對應的記憶體地址都變成不可達,Java垃圾回收自然就會回收這片記憶體,從而不會出現記憶體洩漏的錯誤。

小結

呼應文章開頭提到的《Spring Cloud Netflix Zuul原始碼分析之請求處理篇》,其中就有一個非常重要的類:ZuulServlet,它就是典型的ThreadLocal在實際場景中的運用案例。請看:

 1public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
2    try {
3        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
4        RequestContext context = RequestContext.getCurrentContext();
5        context.setZuulEngineRan();
6
7        try {
8            preRoute();
9        } catch (ZuulException e) {
10            error(e);
11            postRoute();
12            return;
13        }
14        try {
15            route();
16        } catch (ZuulException e) {
17            error(e);
18            postRoute();
19            return;
20        }
21        try {
22            postRoute();
23        } catch (ZuulException e) {
24            error(e);
25            return;
26        }
27
28    } catch (Throwable e) {
29        error(new ZuulException(e, 500"UNHANDLED_EXCEPTION_" + e.getClass().getName()));
30    } finally {
31        RequestContext.getCurrentContext().unset();
32    }
33}
複製程式碼

您有沒有發現,一次HTTP請求經由前置過濾器、路由過濾器、後置過濾器處理完成之後,都會呼叫一個方法,沒錯,就是在finally裡,RequestContext.getCurrentContext().unset()。走進RequestContext一看:

1public void unset({
2    threadLocal.remove();
3}
複製程式碼

看到沒有,神器的remove又出現了。講到這裡,您是否get到ThreadLocal正確的使用"姿勢"呢?

ThreadLocalMap之番外篇

筆者之前寫過關於TreeMap和HashMap的文章,凡是Map的實現,都有自己降低雜湊衝突和解決雜湊衝突的方法。在這裡,ThreadLocalMap是如何處理的呢?請往下看。

如何降低雜湊衝突

相關推薦

ThreadLocal深度解讀

微信公眾號:I am CR7如有問題或建議,請在下方留言;最近更新:2019-01-12 前言 繼上一篇文章《Spring Cloud Netflix Zuul原始碼分析之請求處理篇》中提到的RequestContext使用的兩大神器之一:ThreadLocal,本文特此進行

嵌入式C語言深度解讀C語言的儲存域,作用域,生命週期,連結屬性

***儲存類:    就是儲存型別,描述,C語言變數的儲存地址。    記憶體的管理方式:棧  堆  資料段  bss段  .text段。    一個變數的儲存型別就是描述這個變數儲存在何種記憶體段之

重寫equal()時為什麼也得重寫hashCode()深度解讀equal方法與hashCode方法淵源

轉載請註明出處: 今天這篇文章我們打算來深度解讀一下equal方法以及其關聯方法hashCode(),我們準備從以下幾點入手分析: 1.equals()的所屬以及內部原理(即Object中equals方法的實現原理) 說起equals方法,我們都知道是超類Obje

深度解讀GoogleNetInception V1

能力 翻轉 浪費 對齊 並行運算 bubuko AD 好的 減少 GoogleNet設計的目的 GoogleNet設計的初衷是為了提高在網絡裏面的計算資源的利用率。 Motivation 網絡越大,意味著網絡的參數較多,尤其當數據集很小的時候,網絡更容易發生過擬合。網絡越大

Linux基礎教程linux檔案許可權深度解讀

基本命令——來源於馬哥教育官網1.cut: cat /etc/passwd | cut -d’:’ -f7| uniq -c| sort -nr 2.authconfig 修改加密方式–passalgo=sha256 — update3.scp 上傳檔案-r dir ip:path 傳目錄file ip:p

Linux基礎教程linux文件權限深度解讀

系統 suid權限 absolut 是否 上傳 設置 mask 用戶創建 commond 基本命令——來源於馬哥教育官網1.cut: cat /etc/passwd | cut -d’:’ -f7| uniq -c| sort -nr 2.authconfig 修改加密方式

深度學習GoogLeNet解讀

提出背景  始於LeNet-5,一個有著標準的堆疊式卷積層冰帶有一個或多個全連線層的結構的卷積神經網路。通常使用dropout來針對過擬合問題。  為了提出一個更深的網路,GoogLeNet做到了22層,利用inception結構,這個結構很好地利用了

深度學習AlexNet解讀

為什麼提出? 提出的背景  目前的目標識別任務基本上全是利用的傳統機器學習的方法,為了提升他們的效能。  由於現實中有成千上萬的可變的圖片,現在帶標籤的資料集相對來說還是太小了,因此簡單的識別任務由於這些資料集的尺寸有限,還是獲得了不

dubbo原始碼深度解讀registery模組

前言:dubbo-registry是註冊中心模組,基於註冊中心下發地址的叢集方式,以及對各種註冊中心的抽象。Dubbo的註冊中心提供了多種實現,其實現是基於dubbo的spi的擴充套件機制的,我們也可以直接實現自己的註冊中心。 (一)dubbo-registr

WPF ItemsControl深度解讀Items和ItemsSource

    相信不少WPF開發者對控制元件有一些瞭解,擺脫了早期微軟的MFC,發現WPF程式設計方式更加方便。但是仍然是浩如煙海般的資料需要了解。控制元件是WPF開發繞不開的話題。今天我們就從分析ItemsControl控制元件開始。    ItemsControl可以包含多個項

dubbo原始碼深度解讀remoting模組

前言:remoting模組是遠端通訊模組,相當於Dubbo協議的實現,是一個為Dubbo專案處理底層網路通訊的層。具體結合了netty,mina等進行實現。 一,dubbo-remoting-api 首先結合文件的圖先了解一下基礎介面包的主要類。 1,

深度解讀Java8-lambda表示式方法引用

先看個例子 import java.util.ArrayList; import java.util.Arrays; import static java.util.Comparator.comparing; import java.util.Comparator; im

[論文閱讀]阿里DIN深度興趣網路總體解讀

# [論文閱讀]阿里DIN深度興趣網路之總體解讀 [toc] ## 0x00 摘要 Deep Interest Network(DIN)是阿里媽媽精準定向檢索及基礎演算法團隊在2017年6月提出的。其針對電子商務領域(e-commerce industry)的CTR預估,重點在於充分利用/挖掘使用者歷史

[論文閱讀]阿里DIEN深度興趣進化網路總體解讀

# [論文閱讀]阿里DIEN深度興趣進化網路之總體解讀 [toc] ## 0x00 摘要 之前我們介紹了阿里的深度興趣網路(Deep Interest Network,以下簡稱DIN),一年後阿里再次升級其模型到深度興趣進化網路(Deep Interest Evolution Network,以下簡稱D

深度解讀最流行的優化算法:梯度下降

example 分別是 課程 拓展 高斯分布 正則 當前時間 lam 選擇 深度解讀最流行的優化算法:梯度下降 By 機器之心2016年11月21日 15:08 梯度下降法,是當今最流行的優化(optimization)算法,亦是至今最常用的優化神經網絡的方法。本文旨在

圖的遍歷深度優先和廣度優先

優先 ges sky 深度優先 們的 老師 ear blog earch 圖的遍歷之深度優先和廣度優先 深度優先遍歷 假設給定圖G的初態是所有頂點均未曾訪問過。在G中任選一頂點v為初始出發點(源點),則深度優先遍歷可定義如下:首先訪問出發點v,並將其標記為已訪問過;然後依

AI 新技術革命將如何重塑就業和全球化格局?深度解讀 UN 報告 (下篇)

國際 mmu tar rda under 提高 更多 unit log 歡迎大家前往騰訊雲社區,獲取更多騰訊海量技術實踐幹貨哦~ 相關推薦:AI 新技術革命將如何重塑就業和全球化格局?深度解讀 UN 報告 (上篇)AI 新技術革命將如何重塑就業和全球化格局?深度解讀 U

Jmeter(三十二)Jmeter Question 亂碼解讀

直接 默認 進行 json 字符 blog 文件中 內容 錄制完成   眾所周知,編碼的問題影響著眾多開發者,當然見多不怪。   先扒了一個編碼的原因,也就是為什麽要編碼: 計算機中存儲信息的最小單元是一個字節即 8 個 bit,所以能表示的字符範圍是 0~255 個 人

數據結構(三十一)圖的遍歷深度優先遍歷

width depth idt 廣度優先遍歷 http 如果 搜索 src 技術分享   圖的遍歷和樹的遍歷類似。圖的遍歷是指從圖中的某個頂點出發,對圖中的所有頂點訪問且僅訪問一次的過程。通常有兩種遍歷次序方案:深度優先遍歷和廣度優先遍歷。   一、深度優先遍歷算法描述  

數據結構Java版深度優先-圖(十二)

pac show 下標 增加 ava style AD amp mat 這裏用深度優先遍歷存在矩陣裏面的圖。   深度優先利用的是棧的FIFO特性。為此遍歷到底後,可以找到最相鄰的節點繼續遍歷。實現深度優先,還需要在節點加上一個訪問標識,來確定該節點是否已經被訪問過了。 源