每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal
文章目錄
- ThreadLocal
- 核心API
- ThreadLocal類
- 源碼分析
- set
- get
- remove
- 缺陷
- InheritableThreadLocal
- 源碼解析
- 局限性
- TransmittableThreadLocal
- TransmittableThreadLocal 是什么
- 實現原理
- 測試
ThreadLocal
位于java.lang包中的ThreadLocal 。
多線程訪問同一個共享變量的時候容易出現并發問題,特別是多個線程對一個變量進行寫入的時候,為了保證線程安全,一般使用者在訪問共享變量的時候需要進行額外的同步措施才能保證線程安全性。
ThreadLocal是除了加鎖這種同步方式之外的一種保證一種規避多線程訪問出現線程不安全的方法,當我們在創建一個變量后,如果每個線程對其進行訪問的時候訪問的都是線程自己的變量這樣就不會存在線程不安全問題。
ThreadLocal是JDK包提供的,它提供線程本地變量,如果創建一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的一個副本,在實際多線程操作的時候,操作的是自己本地內存中的變量,從而規避了線程安全問題
核心API
- public T get() 從線程上下文環境中獲取設置的值
- public void set(T value) 將值存儲到線程上下文環境中,供后續使用
- public void remove() 清除線程本地上下文環境
ThreadLocal類
【數據存儲位置】
當線程調用 threadLocal 對象的 set(Object value) 方法時,數據并不是存儲在 ThreadLocal 對象中,而是存儲在 Thread 對象中,這也是 ThreadLocal 的由來,具體存儲在線程對象的threadLocals 屬性中,其類型為 ThreadLocal.ThreadLocalMap
【ThreadLocal.ThreadLocalMap】
Map 結構,即鍵值對,鍵為 threadLocal 對象,值為需要存儲到線程上下文的值(threadLocal#set)方法的參數
源碼分析
set
public void set(T value) {// 獲取當前線程 Thread t = Thread.currentThread();// 獲取線程的 threadLocals 屬性ThreadLocalMap map = getMap(t);// 如果不為空,設置k v if (map != null)map.set(this, value);else// 初始化線程對象的 threadLocals,然后將 threadLocal:value 鍵值對存入線程對象的threadLocals 屬性中。createMap(t, value);}get
public T get() {// 獲取當前線程 Thread t = Thread.currentThread();// 獲取線程的 threadLocals 屬性ThreadLocalMap map = getMap(t);// 如果線程對象的 threadLocals 屬性不為空,則從該 Map 結構中,用 threadLocal 對象為鍵去查找值,如果能找到,則返回其 value 值,否則執行代碼 setInitialValue()if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 果線程對象的 threadLocals 屬性為空,或未從 threadLocals 中找到對應的鍵值對,則調用該方法執行初始化return setInitialValue();} private T setInitialValue() {// 調用 initialValue() 獲取默認初始化值,該方法默認返回 null,子類可以重寫,實現線程本地變量的初始化。T value = initialValue();// 獲取當前線程。Thread t = Thread.currentThread();// 獲取該線程對象的 threadLocals 屬性。ThreadLocalMap map = getMap(t);// 如果不為空,則將 threadLocal:value 存入線程對象的 threadLocals 屬性中。if (map != null)map.set(this, value);else// 否則初始化線程對象的 threadLocals,然后將 threadLocal:value 鍵值對存入線程對象的threadLocals 屬性中。createMap(t, value);return value;} /** * * 初始化線程對象的 threadLocals,然后將 threadLocal:value 鍵值對存入線程對象的threadLocals 屬性中。 */void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}remove
public void remove() {// 獲取該線程對象的 threadLocals 屬性。ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)// 移除m.remove(this);}缺陷
ThreadLocal 無法在父子線程之間傳遞, 看源碼我們也知道了,都是Thread.currentThread.
那我們來證明下吧
public class ThreadLocalTest {private static ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {ThreadLocalTest threadLocalTest = new ThreadLocalTest();threadLocal.set("artisan Test");doSomething();}private static void doSomething() {System.out.println("threadLocal中的對象:" + threadLocal.get());new Thread(()->{System.out.println("開啟子線程");System.out.println("子線程中獲取threadLocal:" + threadLocal.get());}).start();} }InheritableThreadLocal
由于 ThreadLocal 在父子線程交互中子線程無法訪問到存儲在父線程中的值,無法滿足某些場景的需求,比如鏈路跟蹤
為了解決上述問題,JDK 引入了 InheritableThreadLocal,即子線程可以訪問父線程中的線程本地變量,準確的說是子線程可以訪問在創建子線程時父線程當時的本地線程變量,其實現原理是在創建子線程將父線程當前存在的本地線程變量拷貝到子線程的本地線程變量中。
源碼解析
public class InheritableThreadLocal<T> extends ThreadLocal<T> {..... }從類的繼承層次來看,InheritableThreadLocal 只是在 ThreadLocal 的 get、set、remove 流程中,重寫了 getMap、createMap 方法,整體流程與 ThreadLocal 保持一致,所以我們初步來看一下InheritableThreadLocal 是如何重寫上述這兩個方法的。
/*** Get the map associated with a ThreadLocal.** @param t the current thread*/ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}和 ThreadLocal比一比
可以知道 ThreadLocal 操作的是 Thread 對象的 threadLocals 屬性,而 InheritableThreadLocal 操作的是 Thread 對象的 inheritableThreadLocals 屬性
/*** Create the map associated with a ThreadLocal.** @param t the current thread* @param firstValue value for the initial entry of the table.*/void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
createMap 被執行的條件是調用 InheritableThreadLocal#get、set 時如果線程的inheritableThreadLocals 屬性為空時才會被調用
咦 ,看到這里沒啥用啊
InheritableThreadLocal 是如何繼承自父對象的線程本地變量的呢?
那就得看 Thread#init 方法
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name;// 獲取當前線程對象,即待創建的線程的父線程。Thread parent = currentThread();........................// 如果父線程的 inheritableThreadLocals 不為空并且 inheritThreadLocals 為 true(該值默認為true),則使用父線程的 inherit 本地變量的值來創建子線程的 inheritableThreadLocals 結構,即將父線程中的本地變量復制到子線程中if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/* Stash the specified stack size in case the VM cares */this.stackSize = stackSize;/* Set thread ID */tid = nextThreadID();} static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);} private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}}類似于 Map 的復制,只不過其在 Hash 沖突時,不是使用鏈表結構,而是直接在數組中找下一個為 null 的槽位
子線程默認拷貝父線程的方式是淺拷貝,如果需要使用深拷貝,需要使用自定義ThreadLocal,繼承 InheritableThreadLocal 并重寫 childValue 方法
那來驗證下吧
public class InheritableThreadLocalTest {private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();public static void main(String[] args) {InheritableThreadLocalTest threadLocalTest = new InheritableThreadLocalTest();threadLocal.set("artisan InheritableThreadLocal");doSomething();}private static void doSomething() {System.out.println("threadLocal中的對象:" + threadLocal.get());new Thread(()->{System.out.println("開啟子線程");System.out.println("子線程中獲取threadLocal:" + threadLocal.get());}).start();} }
符合預期,在子線程中如愿訪問到了在主線程中設置的本地環境變量。
局限性
InheritableThreadLocal 支持子線程訪問在父線程的核心思想是在創建線程的時候將父線程中的本地變量值復制到子線程,即復制的時機為創建子線程時。
線程池能夠復用線程,減少線程的頻繁創建與銷毀,如果使用 InheritableThreadLocal,那么線程池中的線程拷貝的數據來自于第一個提交任務的外部線程,即后面的外部線程向線程池中提交任務時,子線程訪問的本地變量都來源于第一個外部線程,造成線程本地變量混亂
看個代碼
package com.artisan.threadlocal;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class Service {/*** 模擬tomcat線程池*/private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);/*** 業務線程池,默認Control中異步任務執行線程池*/private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);/*** 線程上下文環境,模擬在Control這一層,設置環境變量,然后在這里提交一個異步任務,模擬在子線程中,是否可以訪問到剛設置的環境變量值。*/private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();public static void main(String[] args) {for(int i = 0; i < 10; i ++ ) { // 模式10個請求,每個請求執行ControlThread的邏輯,其具體實現就是,先輸出父線程的名稱, 然后設置本地環境變量,并將父線程名稱傳入到子線程中,在子線程中嘗試獲取在父線程中的設置的環境變量tomcatExecutors.submit(new ControlThread(i));}//簡單粗暴的關閉線程池try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}businessExecutors.shutdown();tomcatExecutors.shutdown();}/*** 模擬Control任務*/static class ControlThread implements Runnable {private int i;public ControlThread(int i) {this.i = i;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + ":" + i);requestIdThreadLocal.set(i);//使用線程池異步處理任務businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));}}/*** 業務任務,主要是模擬在Control控制層,提交任務到線程池執行*/static class BusinessTask implements Runnable {private String parentThreadName;public BusinessTask(String parentThreadName) {this.parentThreadName = parentThreadName;}@Overridepublic void run() {//如果與上面的能對應上來,則說明正確,否則失敗System.out.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());}} }
子線程中出現出現了線程本地變量混亂的現象
TransmittableThreadLocal
TransmittableThreadLocal 是什么
TransmittableThreadLocal 是阿里巴巴開源的專門解決 InheritableThreadLocal 的局限性,實現線程本地變量在線程池的執行過程中,能正常的訪問父線程設置的線程變量。
實現原理
從InheritableThreadLocal 不支持線程池的根本原因是 InheritableThreadLocal 是在父線程創建子線程時復制的,由于線程池的復用機制,“子線程”只會復制一次。要支持線程池中能訪問提交任務線程的本地變量,其實只需要在父線程在向線程池提交任務時復制父線程的上下環境,那在子線程中就能夠如愿訪問到父線程中的本地遍歷,實現本地環境變量在線程調用之中的透傳,實現鏈路跟蹤,這也就是 TransmittableThreadLocal 最本質的實現原理。
TransmittableThreadLocal 繼承自 InheritableThreadLocal,接下來將從 set 方法為入口,開始探究TransmittableThreadLocal 實現原理
@Overridepublic final void set(T value) {// 首先調用父類的 set 方法,將 value 存入線程本地遍歷,即 Thread 對象的inheritableThreadLocals 中super.set(value);// may set null to remove value 如果 value 為空,則調用 removeValue() 否則調用 addValue。if (null == value) removeValue();else addValue();} private void addValue() {// 當前線程在調用 threadLocal 方法的 set 方法(即向線程本地遍歷存儲數據時),如果需要設置的值不為 null,則調用 addValue 方法,將當前 ThreadLocal 存儲到 TransmittableThreadLocal 的全局靜態變量 holder。if (!holder.get().containsKey(this)) { holder.get().put(this, null); // WeakHashMap supports null value.}} private void removeValue() {holder.get().remove(this);}看看holder
private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {@Overrideprotected Map<TransmittableThreadLocal<?>, ?> initialValue() {return new WeakHashMap<TransmittableThreadLocal<?>, Object>();}@Overrideprotected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue);}};從中可以看出,使用了線程本地變量,內部存放的結構為 Map<TransmittableThreadLocal<?>, ?>,即該對象緩存了線程執行過程中所有的 TransmittableThreadLocal 對象,并且其關聯的值不為空
https://github.com/alibaba/transmittable-thread-local
測試
<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.10.2</version></dependency> package com.artisan.threadlocal;import com.alibaba.ttl.TransmittableThreadLocal; import com.alibaba.ttl.threadpool.TtlExecutors;import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public class TTLTest {/*** 模擬tomcat線程池*/private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);/*** 業務線程池,默認Control中異步任務執行線程池*/private static ExecutorService businessExecutors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4)); // 使用ttl線程池,該框架的使用,請查閱官方文檔。/*** 線程上下文環境,模擬在Control這一層,設置環境變量,然后在這里提交一個異步任務,模擬在子線程中,是否可以訪問到剛設置的環境變量值。*/private static TransmittableThreadLocal<Integer> requestIdThreadLocal = new TransmittableThreadLocal<>();// private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();public static void main(String[] args) {for(int i = 0; i < 10; i ++ ) {tomcatExecutors.submit(new ControlThread(i));}//簡單粗暴的關閉線程池try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}businessExecutors.shutdown();tomcatExecutors.shutdown();}/*** 模擬Control任務*/static class ControlThread implements Runnable {private int i;public ControlThread(int i) {this.i = i;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + ":" + i);requestIdThreadLocal.set(i);//使用線程池異步處理任務businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));}}/*** 業務任務,主要是模擬在Control控制層,提交任務到線程池執行*/static class BusinessTask implements Runnable {private String parentThreadName;public BusinessTask(String parentThreadName) {this.parentThreadName = parentThreadName;}@Overridepublic void run() {//如果與上面的能對應上來,則說明正確,否則失敗System.out.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());}} }參考: 全鏈路壓測必備基礎組件之線程上下文管理之“三劍客”
總結
以上是生活随笔為你收集整理的每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 每日一博 - 使用环形队列实现高效的延时
- 下一篇: 每日一博 - Review线程池