這篇文章是慕課網上一門免費課程《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
關注我的公眾號,和我一起成長~