一、基本使用
首先,TTL是用來解決ITL解決不了的問題而誕生的,所以TTL一定是支持父線程的本地變量傳遞給子線程這種基本操作的,ITL也可以做到,但是前面有講過,ITL在線程池的模式下,就沒辦法再正確傳遞了,所以TTL做出的改進就是即便是在線程池模式下,也可以很好的將父線程本地變量傳遞下去,先來看個例子:
// 需要注意的是,使用TTL的時候,要想傳遞的值不出問題,線程池必須得用TTL加一層代理(下面會講這樣做的目的) private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2)); private static ThreadLocal tl = new TransmittableThreadLocal<>(); //這裏採用TTL的實現 public static void main(String[] args) { new Thread(() -> { String mainThreadName = "main_01"; tl.set(1); executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(1), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(1), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(1), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); sleep(1L); //確保上面的會在tl.set執行之前執行 tl.set(2); // 等上面的線程池第一次啓用完了,父線程再給自己賦值 executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(2), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(2), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(2), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { String mainThreadName = "main_02"; tl.set(3); executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(3), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(3), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(3), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); sleep(1L); //確保上面的會在tl.set執行之前執行 tl.set(4); // 等上面的線程池第一次啓用完了,父線程再給自己賦值 executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(4), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(4), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); executorService.execute(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(4), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }); System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get())); }).start(); } private static void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } }
運行結果:
線程名稱-Thread-2, 變量值=4 本地變量改變之前(3), 父線程名稱-main_02, 子線程名稱-pool-1-thread-1, 變量值=3 線程名稱-Thread-1, 變量值=2 本地變量改變之前(1), 父線程名稱-main_01, 子線程名稱-pool-1-thread-2, 變量值=1 本地變量改變之前(1), 父線程名稱-main_01, 子線程名稱-pool-1-thread-1, 變量值=1 本地變量改變之前(3), 父線程名稱-main_02, 子線程名稱-pool-1-thread-2, 變量值=3 本地變量改變之前(3), 父線程名稱-main_02, 子線程名稱-pool-1-thread-2, 變量值=3 本地變量改變之前(1), 父線程名稱-main_01, 子線程名稱-pool-1-thread-1, 變量值=1 本地變量改變之後(2), 父線程名稱-main_01, 子線程名稱-pool-1-thread-2, 變量值=2 本地變量改變之後(4), 父線程名稱-main_02, 子線程名稱-pool-1-thread-1, 變量值=4 本地變量改變之後(4), 父線程名稱-main_02, 子線程名稱-pool-1-thread-1, 變量值=4 本地變量改變之後(4), 父線程名稱-main_02, 子線程名稱-pool-1-thread-2, 變量值=4 本地變量改變之後(2), 父線程名稱-main_01, 子線程名稱-pool-1-thread-1, 變量值=2 本地變量改變之後(2), 父線程名稱-main_01, 子線程名稱-pool-1-thread-2, 變量值=2
程序有些囉嗦,爲了說明問題,加了很多說明,但至少通過上面的例子,不難發現,兩個主線程裏都使用線程池異步,而且值在主線程裏還發生過改變,測試結果展示一切正常,由此可以知道TTL在使用線程池的情況下,也可以很好的完成傳遞,而且不會發生錯亂。
那麼是不是對普通線程異步也有這麼好的支撐呢?
改造下上面的測試代碼:
private static ThreadLocal tl = new TransmittableThreadLocal<>(); public static void main(String[] args) { new Thread(() -> { String mainThreadName = "main_01"; tl.set(1); new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(1), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(1), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(1), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); sleep(1L); //確保上面的會在tl.set執行之前執行 tl.set(2); // 等上面的線程池第一次啓用完了,父線程再給自己賦值 new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(2), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(2), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(2), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { String mainThreadName = "main_02"; tl.set(3); new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(3), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(3), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之前(3), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); sleep(1L); //確保上面的會在tl.set執行之前執行 tl.set(4); // 等上面的線程池第一次啓用完了,父線程再給自己賦值 new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(4), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(4), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { sleep(1L); System.out.println(String.format("本地變量改變之後(4), 父線程名稱-%s, 子線程名稱-%s, 變量值=%s", mainThreadName, Thread.currentThread().getName(), tl.get())); }).start(); System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get())); }).start(); }
相比第一段測試代碼,這一段的異步全都是普通異步,未採用線程池的方式進行異步,看下運行結果:
本地變量改變之後(4), 父線程名稱-main_02, 子線程名稱-Thread-14, 變量值=4 本地變量改變之前(1), 父線程名稱-main_01, 子線程名稱-Thread-5, 變量值=1 線程名稱-Thread-1, 變量值=2 本地變量改變之前(1), 父線程名稱-main_01, 子線程名稱-Thread-3, 變量值=1 本地變量改變之後(2), 父線程名稱-main_01, 子線程名稱-Thread-11, 變量值=2 本地變量改變之前(3), 父線程名稱-main_02, 子線程名稱-Thread-6, 變量值=3 本地變量改變之後(4), 父線程名稱-main_02, 子線程名稱-Thread-12, 變量值=4 本地變量改變之後(4), 父線程名稱-main_02, 子線程名稱-Thread-10, 變量值=4 本地變量改變之前(3), 父線程名稱-main_02, 子線程名稱-Thread-8, 變量值=3 本地變量改變之前(3), 父線程名稱-main_02, 子線程名稱-Thread-4, 變量值=3 本地變量改變之前(1), 父線程名稱-main_01, 子線程名稱-Thread-7, 變量值=1 線程名稱-Thread-2, 變量值=4 本地變量改變之後(2), 父線程名稱-main_01, 子線程名稱-Thread-9, 變量值=2 本地變量改變之後(2), 父線程名稱-main_01, 子線程名稱-Thread-13, 變量值=2
ok,可以看到,達到了跟第一個測試一致的結果。
到這裏,通過上述兩個例子,TTL的基本使用,以及其解決的問題,我們已經有了初步的瞭解,下面我們來解析一下其內部原理,看看TTL是怎麼完成對ITL的優化的。
二、原理分析
先來看TTL裏面的幾個重要屬性及方法
TTL定義:
public class TransmittableThreadLocal extends InheritableThreadLocal
可以看到,TTL繼承了ITL,意味着TTL首先具備ITL的功能。
再來看看一個重要屬性holder:
/** * 這是一個ITL類型的對象,持有一個全局的WeakMap(weakMap的key是弱引用,同TL一樣,也是爲了解決內存泄漏的問題),裏面存放了TTL對象 * 並且重寫了initialValue和childValue方法,尤其是childValue,可以看到在即將異步時父線程的屬性是直接作爲初始化值賦值給子線程的本地變量對象(TTL)的 */ private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder = new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() { @Override protected Map<TransmittableThreadLocal<?>, ?> initialValue() { return new WeakHashMap<TransmittableThreadLocal<?>, Object>(); } @Override protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) { return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue); } };
再來看下set和get:
//下面的方法均屬於TTL類 @Override public final void set(T value) { super.set(value); if (null == value) removeValue(); else addValue(); } @Override public final T get() { T value = super.get(); if (null != value) addValue(); return value; } private void removeValue() { holder.get().remove(this); //從holder持有的map對象中移除 } private void addValue() { if (!holder.get().containsKey(this)) { holder.get().put(this, null); //從holder持有的map對象中添加 } }
TTL裏先了解上述的幾個方法及對象,可以看出,單純的使用TTL是達不到支持線程池本地變量的傳遞的,通過第一部分的例子,可以發現,除了要啓用TTL,還需要通過TtlExecutors.getTtlExecutorService包裝一下線程池纔可以,那麼,下面就來看看在程序即將通過線程池異步的時候,TTL幫我們做了哪些操作(這一部分是TTL支持線程池傳遞的核心部分):
首先打開包裝類,看下execute方法在執行時做了些什麼。
// 此方法屬於線程池包裝類ExecutorTtlWrapper @Override public void execute(@Nonnull Runnable command) { executor.execute(TtlRunnable.get(command)); //這裏會把Rannable包裝一層,這是關鍵,有些邏輯處理,需要在run之前執行 } // 對應上面的get方法,返回一個TtlRunnable對象,屬於TtLRannable包裝類 @Nullable public static TtlRunnable get(@Nullable Runnable runnable) { return get(runnable, false, false); } // 對應上面的get方法 @Nullable public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) { if (null == runnable) return null; if (runnable instanceof TtlEnhanced) { // 若發現已經是目標類型了(說明已經被包裝過了)直接返回 // avoid redundant decoration, and ensure idempotency if (idempotent) return (TtlRunnable) runnable; else throw new IllegalStateException("Already TtlRunnable!"); } return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun); //最終初始化 } // 對應上面的TtlRunnable方法 private TtlRunnable(@Nonnull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { this.capturedRef = new AtomicReference<Object>(capture()); //這裏將捕獲後的父線程本地變量存儲在當前對象的capturedRef裏 this.runnable = runnable; this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; } // 對應上面的capture方法,用於捕獲當前線程(父線程)裏的本地變量,此方法屬於TTL的靜態內部類Transmitter @Nonnull public static Object capture() { Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>(); for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) { // holder裏目前存放的k-v裏的key,就是需要傳給子線程的TTL對象 captured.put(threadLocal, threadLocal.copyValue()); } return captured; // 這裏返回的這個對象,就是當前將要使用線程池異步出來的子線程,所繼承的本地變量合集 } // 對應上面的copyValue,簡單的將TTL對象裏的值返回(結合之前的源碼可以知道get方法其實就是獲取當前線程(父線程)裏的值,調用super.get方法) private T copyValue() { return copy(get()); } protected T copy(T parentValue) { return parentValue; }
結合上述代碼,大致知道了在線程池異步之前需要做的事情,其實就是把當前父線程裏的本地變量取出來,然後賦值給Rannable包裝類裏的capturedRef屬性,到此爲止,下面會發生什麼,我們大致上可以猜出來了,接下來大概率會在run方法裏,將這些捕獲到的值賦給子線程的holder賦對應的TTL值,那麼我們繼續往下看Rannable包裝類裏的run方法是怎麼實現的:
//run方法屬於Rannable的包裝類TtlRunnable @Override public void run() { Object captured = capturedRef.get(); // 獲取由之前捕獲到的父線程變量集 if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) { throw new IllegalStateException("TTL value reference is released after run!"); } /** * 重點方法replay,此方法用來給當前子線程賦本地變量,返回的backup是此子線程原來就有的本地變量值(原生本地變量,下面會詳細講), * backup用於恢復數據(如果任務執行完畢,意味着該子線程會歸還線程池,那麼需要將其原生本地變量屬性恢復) */ Object backup = replay(captured); try { runnable.run(); // 執行異步邏輯 } finally { restore(backup); // 結合上面對於replay的解釋,不難理解,這個方法就是用來恢復原有值的 } }
根據上述代碼,我們看到了TTL在異步任務執行前,會先進行賦值操作(就是拿着異步發生時捕獲到的父線程的本地變量,賦給自己),當任務執行完,就恢復原生的自己本身的線程變量值。
下面來具體看這倆方法:
//下面的方法均屬於TTL的靜態內部類Transmittable @Nonnull public static Object replay(@Nonnull Object captured) { @SuppressWarnings("unchecked") Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured; //使用此線程異步時捕獲到的父線程裏的本地變量值 Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>(); //當前線程原生的本地變量,用於使用完線程後恢復用 //注意:這裏循環的是當前子線程原生的本地變量集合,與本方法相反,restore方法裏循環這個holder是指:該線程運行期間產生的變量+父線程繼承來的變量 for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next(); TransmittableThreadLocal<?> threadLocal = next.getKey(); backup.put(threadLocal, threadLocal.get()); // 所有原生的本地變量都暫時存儲在backup裏,用於之後恢復用 /** * 檢查,如果捕獲到的線程變量裏,不包含當前原生變量值,則從當前原生變量裏清除掉,對應的線程本地變量也清掉 * 這就是爲什麼會將原生變量保存在backup裏的原因,爲了恢復原生值使用 * 那麼,爲什麼這裏要清除掉呢?因爲從使用這個子線程做異步那裏,捕獲到的本地變量並不包含原生的變量,當前線程 * 在做任務時的首要目標,是將父線程裏的變量完全傳遞給任務,如果不清除這個子線程原生的本地變量, * 意味着很可能會影響到任務裏取值的準確性。 * * 打個比方,有ttl對象tl,這個tl在線程池的某個子線程裏存在對應的值2,當某個主線程使用該子線程做異步任務時 * tl這個對象在當前主線程裏沒有值,那麼如果不進行下面這一步的操作,那麼在使用該子線程做的任務裏就可以通過 * 該tl對象取到值2,不符合預期 */ if (!capturedMap.containsKey(threadLocal)) { iterator.remove(); threadLocal.superRemove(); } } // 這一步就是直接把父線程本地變量賦值給當前線程了(這一步起就刷新了holder裏的值了,具體往下看該方法,在異步線程運行期間,還可能產生別的本地變量,比如在真正的run方法內的業務代碼,再用一個tl對象設置一個值) setTtlValuesTo(capturedMap); // 這個方法屬於擴展方法,ttl本身支持重寫異步任務執行前後的操作,這裏不再具體贅述 doExecuteCallback(true); return backup; } // 結合之前Rannable包裝類的run方法來看,這個方法就是使用上面replay記錄下的原生線程變量做恢復用的 public static void restore(@Nonnull Object backup) { @SuppressWarnings("unchecked") Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup; // call afterExecute callback doExecuteCallback(false); // 注意,這裏的holder取出來的,實際上是replay方法設置進去的關於父線程裏的所有變量(結合上面來看,就是:該線程運行期間產生的變量+父線程繼承來的變量) for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next(); TransmittableThreadLocal<?> threadLocal = next.getKey(); /** * 同樣的,如果子線程原生變量不包含某個父線程傳來的對象,那麼就刪除,可以思考下,這裏的清除跟上面replay裏的有什麼不同? * 這裏會把不屬於原生變量的對象給刪除掉(這裏被刪除掉的可能是父線程繼承下來的,也可能是異步任務在執行時產生的新值) */ if (!backupMap.containsKey(threadLocal)) { iterator.remove(); threadLocal.superRemove(); } } // 同樣調用這個方法,進行值的恢復 setTtlValuesTo(backupMap); } // 真正給當前子線程賦值的方法,對應上面的setTtlValuesTo方法 private static void setTtlValuesTo(@Nonnull Map<TransmittableThreadLocal<?>, Object> ttlValues) { for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : ttlValues.entrySet()) { @SuppressWarnings("unchecked") TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey(); threadLocal.set(entry.getValue()); //賦值,注意,從這裏開始,子線程的holder裏的值會被重新賦值刷新,可以參照上面ttl的set方法的實現 } }
ok,到這裏基本上把TTL比較核心的代碼看完了,下面整理下整個流程,這是官方給出的時序圖:
圖2-1
上圖第一行指的是類名稱,下面的流程指的是類所做的事情,根據上面羅列出來的源碼,結合這個時序圖,可以比較直觀一些的理解整個流程。
三、TTL中線程池子線程原生變量的產生
這一節是爲了驗證上面replay和restore,現在通過一個例子來驗證下,先把源碼down下來,在源碼的restore和replay上分別加上輸出語句,遍歷holder:
//replay前後打印holder裏面的值 public static Object replay(@Nonnull Object captured) { @SuppressWarnings("unchecked") Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured; Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>(); System.out.println("--------------------replay前置,當前拿到的holder裏的TTL列表"); for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next(); TransmittableThreadLocal<?> threadLocal = next.getKey(); System.out.println(String.format("replay前置裏拿到原生的ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get())); } for...//代碼省略,具體看上面 setTtlValuesTo(capturedMap); doExecuteCallback(true); System.out.println("--------------------reply後置,當前拿到的holder裏的TTL列表"); for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next(); TransmittableThreadLocal<?> threadLocal = next.getKey(); System.out.println(String.format("replay後置裏拿到原生的ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get())); } return backup; } //restore前後打印holder裏面的值 public static void restore(@Nonnull Object backup) { @SuppressWarnings("unchecked") Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup; // call afterExecute callback doExecuteCallback(false); System.out.println("--------------------restore前置,當前拿到的holder裏的TTL列表"); for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next(); TransmittableThreadLocal<?> threadLocal = next.getKey(); System.out.println(String.format("restore前置裏拿到當前線程內變量,ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get())); } for...//省略代碼,具體具體看上面 setTtlValuesTo(backupMap); System.out.println("--------------------restore後置,當前拿到的holder裏的TTL列表"); for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next(); TransmittableThreadLocal<?> threadLocal = next.getKey(); System.out.println(String.format("restore後置裏拿到當前線程內變量,ttl_k=%s, ttl_value=%s", threadLocal.hashCode(), threadLocal.get())); } }
代碼這樣做的目的,就是要說明線程池所謂的原生本地變量是怎麼產生的,以及replay和restore是怎麼設置和恢復的,下面來看個簡單的例子:
private static ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1)); private static ThreadLocal tl = new TransmittableThreadLocal(); private static ThreadLocal tl2 = new TransmittableThreadLocal(); public static void main(String[] args) throws InterruptedException { tl.set(1); tl2.set(2); executorService.execute(new Runnable() { @Override public void run() { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } }); }
運行結果如下:
--------------------replay前置,當前拿到的holder裏的TTL列表 replay前置裏拿到原生的ttl_k=1259475182, ttl_value=2 replay前置裏拿到原生的ttl_k=929338653, ttl_value=1 --------------------reply後置,當前拿到的holder裏的TTL列表 replay後置裏拿到原生的ttl_k=1259475182, ttl_value=2 replay後置裏拿到原生的ttl_k=929338653, ttl_value=1 --------------------restore前置,當前拿到的holder裏的TTL列表 restore前置裏拿到當前線程內變量,ttl_k=1259475182, ttl_value=2 restore前置裏拿到當前線程內變量,ttl_k=929338653, ttl_value=1 --------------------restore後置,當前拿到的holder裏的TTL列表 restore後置裏拿到當前線程內變量,ttl_k=1259475182, ttl_value=2 restore後置裏拿到當前線程內變量,ttl_k=929338653, ttl_value=1
我們會發現,原生值產生了,從異步開始,就確定了線程池裏的線程具備了1和2的值,那麼,再來改動下上面的測試代碼:
public static void main(String[] args) throws InterruptedException { tl.set(1); executorService.execute(new Runnable() { @Override public void run() { try { Thread.sleep(100L); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread.sleep(1000L); tl2.set(2);//較第一次換下位置,換到第一次使用線程池後執行(這意味着下面這次異步不會再觸發Thread的init方法了) System.out.println("---------------------------------------------------------------------------------"); executorService.execute(new Runnable() { @Override public void run() { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } }); }
運行結果爲:
--------------------replay前置,當前拿到的holder裏的TTL列表 replay前置裏拿到原生的ttl_k=929338653, ttl_value=1 --------------------reply後置,當前拿到的holder裏的TTL列表 replay後置裏拿到原生的ttl_k=929338653, ttl_value=1 --------------------restore前置,當前拿到的holder裏的TTL列表 restore前置裏拿到當前線程內變量,ttl_k=929338653, ttl_value=1 --------------------restore後置,當前拿到的holder裏的TTL列表 restore後置裏拿到當前線程內變量,ttl_k=929338653, ttl_value=1 --------------------------------------------------------------------------------- --------------------replay前置,當前拿到的holder裏的TTL列表 replay前置裏拿到原生的ttl_k=929338653, ttl_value=1 --------------------reply後置,當前拿到的holder裏的TTL列表 replay後置裏拿到原生的ttl_k=1020371697, ttl_value=2 replay後置裏拿到原生的ttl_k=929338653, ttl_value=1 --------------------restore前置,當前拿到的holder裏的TTL列表 restore前置裏拿到當前線程內變量,ttl_k=1020371697, ttl_value=2 restore前置裏拿到當前線程內變量,ttl_k=929338653, ttl_value=1 --------------------restore後置,當前拿到的holder裏的TTL列表 restore後置裏拿到當前線程內變量,ttl_k=929338653, ttl_value=1
可以發現,第一次異步時,只有一個值被傳遞了下去,然後第二次異步,新加了一個tl2的值,但是看第二次異步的打印,會發現,restore恢復後,仍然是第一次異步發生時放進去的那個tl的值。
通過上面的例子,基本可以確認,所謂線程池內線程的本地原生變量,其實是第一次使用線程時被傳遞進去的值,我們之前有說過TTL是繼承至ITL的,之前的文章也說過,線程池第一次啓用時是會觸發Thread的init方法的,也就是說,在第一次異步時拿到的主線程的變量會被傳遞給子線程,作爲子線程的原生本地變量保存起來,後續是replay操作和restore操作也是圍繞着這個原生變量(即原生holder裏的值)來進行設置、恢復的,設置的是當前父線程捕獲到的本地變量,恢復的是子線程原生本地變量。
holder裏持有的可以理解就是當前線程內的所有本地變量,當子線程將異步任務執行完畢後,會執行restore進行恢復原生本地變量,具體參照上面的代碼和測試代碼。
四、總結
到這裏基本上確認了TTL是如何進行線程池傳值的,以及被包裝的run方法執行異步任務之前,會使用replay進行設置父線程裏的本地變量給當前子線程,任務執行完畢,會調用restore恢復該子線程原生的本地變量(目前原生本地變量的產生,就只碰到上述測試代碼中的這一種情況,即線程第一次使用時通過ITL屬性以及Thread的init方法傳給子線程,還不太清楚有沒有其他方式設置)。
其實,正常程序裏想要完成線程池上下文傳遞,使用TL就足夠了,我們可以效仿TTL包裝線程池對象的原理,進行值傳遞,異步任務結束後,再remove,以此類推來完成線程池值傳遞,不過這種方式過於單純,且要求上下文爲只讀對象,否則子線程存在寫操作,就會發生上下文污染。
TTL項目地址(可以詳細瞭解下它的其他特性和用法):https://github.com/alibaba/transmittable-thread-local