并發編程之 ThreadLocal( 二 )


使用ThreadLocal 的好處

  • 達到線程安全
  • 不需要加鎖,提高執行效率
  • 合理利用內存,節省開銷
以下代碼,我們構建了一個內部類 ThreadSafeFormatter 類 , 在類內部定義 ThreadLocal 的成員變量,并重寫了 initialValue 方法,返回的參數就是 new 出來的 SimpleDateFormat 對象 。
public class ThreadLocalTest {public static ExecutorService threadPool = Executors.newFixedThreadPool(10);public static void main(String[] args) {for (int i = 0; i < 1000; i++) {int finalI = i;threadPool.submit(() -> System.out.println(new ThreadLocalTest().sec2Date(finalI)));}}private String sec2Date(int seconds) {// 在 ThreadLocal 第一個 get 的時候把對象初始化出來,對象的初始化時機可以由我們控制SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();return dateFormat.format(seconds * 1000);}static class ThreadSafeFormatter {// 方式一(原始方式)public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {// 初始化@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");}};// 方式二(Lambda表達式)public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));}}輸出結果:
并發編程之 ThreadLocal

文章插圖
結果中我們可以看出,沒有輸出重復的時間值(可以多運行幾次觀察下),因此我們通過 ThreadLocal 這種方式就達到了線程安全,并且還節省了系統的開銷,合理利用了內存 。
由此我們可以得到一個結論:每個線程的 SimpleDateFormat 是獨立的,一共有 10 個 。每個線程會平均執行 100 個任務 , 每個線程之間都是復用一個 SimpleDateFormat 對象 。
ThreadLocal 源碼分析在了解 ThreadLocal 源碼之前 , 我們先了解以下 Thread,ThreadLocalMap 以及 ThreadLocal三者之間的關系 。
首先,我們創建的每一個 Thread 對象中都持有一個 ThreadLocalMap 成員變量,而 ThreadLocalMap 中可以存放著很多的 key 為 ThreadLocal 的鍵值對 。
并發編程之 ThreadLocal

文章插圖

并發編程之 ThreadLocal

文章插圖
主要方法介紹
  • T initialValue() : 初始化,返回當前線程對應的“初始值”,這是一個延遲加載的方法,只有在調用get的時候,才會觸發 。
  • void set(T t) : 為這個線程設置一個新值 。
  • T get() : 得到這個線程對應的value 。如果是首次調用 get(),則會調用 initialize 來得到這個值 。
  • void remove() :刪除對應這個線程的值 。
initialValueSimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
在上述代碼,我們并沒有顯式地調用這個 initialValue 方法,而是調用了 get 方法,而在 get 方法中,它會去調用
setInitialValue 方法,在 該方法內部它才會去調用我們重寫的 initialValue 方法 。
并發編程之 ThreadLocal

文章插圖

并發編程之 ThreadLocal

文章插圖
如果沒有重寫 initialValue 時,默認會返回 null
并發編程之 ThreadLocal

文章插圖
如果線程先前調用了set方法,在這種情況下,不會為線程調用本 initialValue 方法,而是直接用之前 set 進去的值 。
在通常情況下,每個線程最多只能調用一次 initialValue 方法,但是如果已經調用了 remove 方法之后,再調用 get 方法,則可以再次調用 initialValue 方法 。
getget 方法是先取出當前線程的 ThreadLocalMap ,然后調用 map.getEntry 方法,把本 ThreadLocal 的引用作為參數傳入,取出 map 中屬于本 ThreadLocal 的value 。
public T get() {// 獲取當前線程Thread t = Thread.currentThread();// 獲取當前線程的threadLocals 成員變量ThreadLocalMap map = getMap(t);if (map != null) {// this 指的是 ThreadLocal 對象,通過 map.getEntry 來獲取我們通過 set 方法設置進去的 value 值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}set跟 get 一樣,同樣是先獲取當前線程的引用,然后再獲取當前線程的 threadLocals 成員變量,如果 threadLocals 為null,即還未初始化 , 就會執行 createMap 方法來進行初始化 。

推薦閱讀