這篇文章是慕課網上一門免費課程《ThreadLocal》的觀後總結。這門課將ThreadLocal
講得非常清晰易懂,又深入底層原理和設計思想,是我看過的最好的ThreadLocal的資料,現在把用自己的話,把它整理成文字版本。
總共預計產出四篇文章,這是第一篇。
一致性問題
什麼是一致性問題?
多執行緒充分利用了多核CPU的威力,為我們程式提供了很高的效能。但是有時候,我們需要多個執行緒互相協作,這裡可能就會涉及到資料一致性的問題。
資料一致性指問題的是:發生在「多個主體」對「同一份」資料無法達成「共識」。這裡的多個主體,可能是多執行緒,也可能是多個伺服器節點。
當然了,這裡的“多個主體”也可以指朋友之間,夫妻之間,所謂“道不同,不相為謀”,說的就是這個理。
資料一致性問題是我們使用分散式或者多執行緒帶來的代價,使得程式或系統變得複雜,那我們有什麼辦法可以解決它呢?
如何解決一致性問題?
在我們解決一致性問題的時候,大概有這樣幾種思路。
第一種是「排隊」,如果兩個人對一個問題的看法不一致,那就排成一隊,一個人一個人去修改它,這樣後面一個人總是能夠得到前面一個人修改後的值,資料也就總是一致的了。我們在作業系統中的鎖、互斥量、管程、屏障等等概念,都是利用了排隊的思想。
排隊雖然能夠很好的確保資料一致性,但效能非常低。
第二種是「投票」,投票的話,多個人可以同時去做一件決策,或者同時去修改資料,但最終誰修改成功,是用投票來決定的。這個方式很高效,但它也會產生很多問題,比如網路中斷、欺詐等等。想要通過投票達到一致性非常複雜,往往需要嚴格的數學理論來證明,還需要中間有一些“信使”不斷來來回回傳遞訊息,這中間也會有一些效能的開銷。
我們在分散式系統中常見的Paxos和Raft演算法,就是使用投票來解決一致性問題的。感興趣的同學可以去我的個人網站閱讀我之前寫的關於這兩個演算法的文章。
第三種是「避免」。既然保證資料一致性很難,那我能不能通過一些手段,去避免多個執行緒之間產生一致性問題呢?我們程式設計師熟悉的git就是這個實現,大家在本地分散式修改同一個檔案,最後通過版本控制和“衝突解決”去解決這個問題。
而我們今天的正題,ThreadLocal,也是使用的“避免”這種方式。
❝我們不能避免所有的資料不一致問題,所以我們還是需要學習排隊和投票,去解決不同場景的資料不一致問題。
❞
ThreadLocal是什麼?
定義
首先我們上定義:ThreadLocal提供了「執行緒區域性變數」,一個執行緒區域性變數在多個執行緒中,分別有獨立的值(副本)。
是不是有些看不懂?我剛開始瞭解ThreadLocal的時候,也有點不明白這句話是什麼意思。最大的疑惑是:既然是每個執行緒獨有的,那我幹嘛不直接在呼叫執行緒的時候,在相應的方法裡面宣告和使用這個區域性變數?
後來才明白,同一個執行緒可能會呼叫到很多不同的類和方法,你可能需要在不同的地方,用到這個變數。如果自己去實現這麼一個功能,成本其實挺大的。
ThreadLocal是一個可以開箱即用、無額外開銷、執行緒安全的工具類,可以完美解決這個問題。
ThreadLocal並不是Java語言獨有的,在幾乎所有提供多執行緒特徵的語言裡面,都會有ThreadLocal的實現。在Java中,ThreadLocal中用雜湊表來實現的。
執行緒模型
這個圖能夠比較直觀地解釋ThreadLocal的執行緒模型。
其實並不複雜。左邊的黑色大圓圈代表一個程序。程序裡有一個執行緒表,紅色波浪線代表一個個執行緒。
對於每一個執行緒來說,都有自己的獨佔資料。這些獨佔資料是程序來分配的,對於Java來說,獨佔資料很多都是在Thread類裡面分配的,而每一個執行緒裡面都有一個ThreadLocalMap的物件,它本身是一個雜湊表,裡面會放一些執行緒的區域性變數(紅色長方形)。ThreadLocal的核心也是這個ThreadLocalMap。
相關原始碼:
// Thread類裡的變數:
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocalMap的定義:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// ...
}
複製程式碼
基本API
基本API主要分成四個部分,分別是:
建構函式ThreadLocal() 初始化initialValue() 訪問器get/set 回收remove
建構函式是一個泛型的,傳入的型別是你要使用的區域性變數變數的型別。初始化initialValue()
用於如果你沒有呼叫set()
方法的時候,呼叫get()
方法返回的預設值。如果不過載初始化方法,會返回null
。
如果呼叫了set()方法,再呼叫get()方法,就不會呼叫initialValue()方法。
如果呼叫了set(),再呼叫remove(),再呼叫get(),是會呼叫initialValue()的。
❝JDK 8提供了靜態方法withInitial來進行更好的初始化。
❞
示例程式碼:
public class ThreadLocalDemo {
public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> {
System.out.println("invoke initial value");
return "default value";
});
public static void main(String[] args) throws InterruptedException {
new Thread(() ->{
THREAD_LOCAL.set("first thread");
System.out.println(THREAD_LOCAL.get());
}).start();
new Thread(() ->{
THREAD_LOCAL.set("second thread");
System.out.println(THREAD_LOCAL.get());
}).start();
new Thread(() ->{
THREAD_LOCAL.set("third thread");
THREAD_LOCAL.remove();
System.out.println(THREAD_LOCAL.get());
}).start();
new Thread(() ->{
System.out.println(THREAD_LOCAL.get());
}).start();
SECONDS.sleep(1L);
}
}
// 輸出:
first thread
second thread
invoke initial value
default value
invoke initial value
default value
複製程式碼
4種核心場景
在實際專案中,ThreadLocal一般用來做什麼呢?這裡總結四種核心的應用場景。
資源持有
比如我們有三個不同的類。在一次Web請求中,會在不同的地方,不同的時候,呼叫這三個類的例項。但使用者是同一個,使用者資料可以儲存在「一個執行緒」裡。
這個時候,我們可以在程式1把使用者資料放進ThreadLocalMap裡,然後在程式2和程式3裡面去用它。
這樣做的優勢在於:持有執行緒資源供執行緒的各個部分使用,全域性獲取,降低「程式設計難度」。
執行緒一致
這裡以JDBC為例。我們經常會用到事務,它是怎麼實現的呢?
原來,我們每次對資料庫操作,都會走JDBC getConnection,JDBC保證只要你是同一個執行緒過來的請求,不管是在哪個part,都返回的是同一個連線。這個就是使用ThreadLocal來做的。
當一個part過來的時候,JDBC會去看ThreadLocal裡是不是已經有這個執行緒的連線了,如果有,就直接返回;如果沒有,就從連線池請求分配一個連線,然後放進ThreadLocal裡。
這樣就可以保證一個事務的所有part都在一個連線裡。TheadLocal可以幫助它維護這種一致性,降低「程式設計難度」。
執行緒安全
假設我們一個執行緒的呼叫鏈路比較長。在中途中出現異常怎麼做?我們可以在出錯的時候,把錯誤資訊放到ThreadLocal裡面,然後在後續的鏈路去使用這個值。使用TheadLocal可以保證多個執行緒在處理這個場景的時候保證執行緒安全。
併發計算
如果我們有一個大的任務,可以把它拆分成很多小任務,分別計算,然後最終把結果彙總起來。如果是分散式計算,可能是先儲存在自己的節點裡。而如果是單機下的多執行緒計算,可以把每個執行緒的計算結果放進ThreadLocal裡面,最後取出來彙總。
那麼問題來了,怎麼取出ThreadLocal的所有執行緒的值?且看下篇文章分析。
關於作者
我是Yasin,一個有顏有料又有趣的程式設計師。
微信公眾號:編了個程
個人網站:https://yasinshaw.com
關注我的公眾號,和我一起成長~