日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解析

發布時間:2023/12/10 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

ThreadLocal系列之InheritableThreadLocal的使用及原理解析(源碼基于java8)

上一篇:ThreadLocal系列(一)-ThreadLocal的使用及原理解析

下一篇:ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析

一、基本使用

我們繼續來看之前寫的例子:

private static ThreadLocal tl = new ThreadLocal<>();public static void main(String[] args) throws Exception {tl.set(1);System.out.println(String.format("當前線程名稱: %s, main方法內獲取線程內數據為: %s",Thread.currentThread().getName(), tl.get()));fc();new Thread(() -> {fc();}).start();Thread.sleep(1000L); //保證下面fc執行一定在上面異步代碼之后執行fc(); //繼續在主線程內執行,驗證上面那一步是否對主線程上下文內容造成影響}private static void fc() {System.out.println(String.format("當前線程名稱: %s, fc方法內獲取線程內數據為: %s",Thread.currentThread().getName(), tl.get()));}

輸出為:

當前線程名稱: main, main方法內獲取線程內數據為: 1 當前線程名稱: main, fc方法內獲取線程內數據為: 1 當前線程名稱: Thread-0, fc方法內獲取線程內數據為: null 當前線程名稱: main, fc方法內獲取線程內數據為: 1

我們會發現,父線程的本地變量是無法傳遞給子線程的,這當然是正常的,因為線程本地變量來就不應該相互有交集,但是有些時候,我們的確是需要子線程里仍然可以獲取到父線程里的本地變量,現在就需要借助TL的一個子類:InheritableThreadLocal(下面簡稱ITL),來完成上述要求 現在我們將例子里的

private static ThreadLocal tl = new ThreadLocal<>();

改為:

private static ThreadLocal tl = new InheritableThreadLocal<>();

然后我們再來運行下結果:

當前線程名稱: main, main方法內獲取線程內數據為: 1 當前線程名稱: main, fc方法內獲取線程內數據為: 1 當前線程名稱: Thread-0, fc方法內獲取線程內數據為: 1 當前線程名稱: main, fc方法內獲取線程內數據為: 1

可以發現,子線程里已經可以獲得父線程里的本地變量了。

結合之前講的TL的實現,簡單理解起來并不難,基本可以認定,是在創建子線程的時候,父線程的ThreadLocalMap(下面簡稱TLMap)里的值遞給了子線程,子線程針對上述tl對象持有的k-v進行了copy,其實這里不是真正意義上對象copy,只是給v的值多了一條子線程TLMap的引用而已,v的值在父子線程里指向的均是同一個對象,因此任意線程改了這個值,對其他線程是可見的,為了驗證這一點,我們可以改造以上測試代碼:

private static ThreadLocal tl = new InheritableThreadLocal<>();private static ThreadLocal tl2 = new InheritableThreadLocal<>();public static void main(String[] args) throws Exception {tl.set(1);Hello hello = new Hello();hello.setName("init");tl2.set(hello);System.out.println(String.format("當前線程名稱: %s, main方法內獲取線程內數據為: tl = %s,tl2.name = %s",Thread.currentThread().getName(), tl.get(), tl2.get().getName()));fc();new Thread(() -> {Hello hello1 = tl2.get();hello1.setName("init2");fc();}).start();Thread.sleep(1000L); //保證下面fc執行一定在上面異步代碼之后執行fc(); //繼續在主線程內執行,驗證上面那一步是否對主線程上下文內容造成影響}private static void fc() {System.out.println(String.format("當前線程名稱: %s, fc方法內獲取線程內數據為: tl = %s,tl2.name = %s",Thread.currentThread().getName(), tl.get(), tl2.get().getName()));}

輸出結果為:

當前線程名稱: main, main方法內獲取線程內數據為: tl = 1,tl2.name = init 當前線程名稱: main, fc方法內獲取線程內數據為: tl = 1,tl2.name = init 當前線程名稱: Thread-0, fc方法內獲取線程內數據為: tl = 1,tl2.name = init2 當前線程名稱: main, fc方法內獲取線程內數據為: tl = 1,tl2.name = init2

可以確認,子線程里持有的本地變量跟父線程里那個是同一個對象。

?

二、原理分析

通過上述的測試代碼,基本可以確定父線程的TLMap被傳遞到了下一級,那么我們基本可以確認ITL是TL派生出來專門解決線程本地變量父傳子問題的,那么下面通過源碼來分析一下ITL到底是怎么完成這個操作的。

先來了解下Thread類,上節說到,其實最終線程本地變量是通過TLMap存儲在Thread對象內的,那么來看下Thread對象內關于TLMap的兩個屬性:

ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Thread類里其實有兩個TLMap屬性,第一個就是普通TL對象為其賦值,第二個則由ITL對象為其賦值,來看下TL的set方法的實現,這次針對該方法介紹下TL子類的相關方法實現:

// TL的set方法,如果是子類的實現,那么獲取(getMap)和初始化賦值(createMap)都是ITL對象里的方法// 其余操作不變(因為hash計算、查找、擴容都是TLMap里需要做的,這里子類ITL只起到一個為Thread對象里哪個TLMap屬性賦值的作用)public void set(T value) {Thread t = Thread.currentThread();ThreadLocal.ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}// ITL里getMap方法的實現ThreadLocal.ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals; //返回的其實是Thread對象的inheritableThreadLocals屬性}// ITL里createMap方法的實現void createMap(Thread t, T firstValue) {// 也是給Thread的inheritableThreadLocals屬性賦值t.inheritableThreadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);}

而inheritableThreadLocals里的信息通過Thread的init方法是可以被傳遞下去的:

// 初始化一個Thread對象時的代碼段(Thread類的init方法)Thread parent = currentThread();if (parent.inheritableThreadLocals != null){ //可以看到,如果父線程存在inheritableThreadLocals的時候,會賦值給子線程(當前正在被初始化的線程)// 利用父線程的TLMap對象,初始化一個TLMap,賦值給自己的inheritableThreadLocals(這就意味著這個TLMap里的值會一直被傳遞下去)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);}// 看下TL里對應的方法static ThreadLocal.ThreadLocalMap createInheritedMap(ThreadLocal.ThreadLocalMap parentMap) {return new ThreadLocal.ThreadLocalMap(parentMap); //這里就開始初始化TLMap對象了}// 根據parentMap來進行初始化子線程的TLMap對象private ThreadLocalMap(ThreadLocal.ThreadLocalMap parentMap) {ThreadLocal.ThreadLocalMap.Entry[] parentTable = parentMap.table; //拿到父線程里的哈希表int len = parentTable.length;setThreshold(len); // 設置閾值(具體方法參考上一篇)table = new ThreadLocal.ThreadLocalMap.Entry[len];for (int j = 0; j < len; j++) {ThreadLocal.ThreadLocalMap.Entry e = parentTable[j]; //將父線程里的Entry取出if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); //獲取keyif (key != null) {Object value = key.childValue(e.value); //獲取valueThreadLocal.ThreadLocalMap.Entry c = new ThreadLocal.ThreadLocalMap.Entry(key, value); //根據k-v重新生成一個Entryint h = key.threadLocalHashCode & (len - 1); //計算哈希值while (table[h] != null)h = nextIndex(h, len); //線性探查解決哈希沖突問題(具體方法參考上一篇)table[h] = c; //找到合適的位置后進行賦值size++;}}}}// ITL里的childValue的實現protected T childValue(T parentValue) {return parentValue; //直接將父線程里的值返回}

看過上述代碼后,現在關于ITL的實現我們基本上有了清晰的認識了,根據其實現性質,可以總結出在使用ITL時可能存在的問題:

1.線程不安全

寫在前面:這里討論的線程不安全對象不包含Integer等類型,因為這種對象被重新賦值,變掉的是整個引用,這里說的是那種不改變對象引用,直接可以修改其內容的對象(典型的就是自定義對象的set方法)

如果說線程本地變量是只讀變量不會受到影響,但是如果是可寫的,那么任意子線程針對本地變量的修改都會影響到主線程的本地變量(本質上是同一個對象),參考上面的第三個例子,子線程寫入后會覆蓋掉主線程的變量,也是通過這個結果,我們確認了子線程TLMap里變量指向的對象和父線程是同一個。

2.線程池中可能失效

按照上述實現,在使用線程池的時候,ITL會完全失效,因為父線程的TLMap是通過init一個Thread的時候進行賦值給子線程的,而線程池在執行異步任務時可能不再需要創建新的線程了,因此也就不會再傳遞父線程的TLMap給子線程了。

針對上述2,我們來做個實驗,來證明下猜想:

// 為了方便觀察,我們假定線程池里只有一個線程private static ExecutorService executorService = Executors.newFixedThreadPool(1);private static ThreadLocal tl = new InheritableThreadLocal<>();public static void main(String[] args) {tl.set(1);System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get()));executorService.execute(()->{System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get()));});executorService.execute(()->{System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get()));});System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get()));}

輸出結果為:

線程名稱-main, 變量值=1 線程名稱-pool-1-thread-1, 變量值=1 線程名稱-main, 變量值=1 線程名稱-pool-1-thread-1, 變量值=1

會發現,并沒有什么問題,和我們預想的并不一樣,原因是什么呢?因為線程池本身存在一個初始化的過程,第一次使用的時候發現里面的線程數(worker數)少于核心線程數時,會進行創建線程,既然是創建線程,一定會執行Thread的init方法,參考上面提到的源碼,在第一次啟用線程池的時候,類似做了一次new Thread的操作,因此是沒有什么問題的,父線程的TLMap依然可以傳遞下去。

現在我們改造下代碼,把tl.set(1)改到第一次啟用線程池的下面一行,然后再看看:

public static void main(String[] args) throws Exception{System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get()));executorService.execute(()->{System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get()));});tl.set(1); // 等上面的線程池第一次啟用完了,父線程再給自己賦值executorService.execute(()->{System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get()));});System.out.println(String.format("線程名稱-%s, 變量值=%s", Thread.currentThread().getName(), tl.get()));}

輸出結果為:

線程名稱-main, 變量值=null 線程名稱-main, 變量值=1 線程名稱-pool-1-thread-1, 變量值=null 線程名稱-pool-1-thread-1, 變量值=null

很明顯,第一次啟用時沒有遞進去的值,在后續的子線程啟動時就再也傳遞不進去了。

?

但是,在實際項目中我們大多數采用線程池進行做異步任務,假如真的需要傳遞主線程的本地變量,使用ITL的問題顯然是很大的,因為是有極大可能性拿不到任何值的,顯然在實際項目中,ITL的位置實在是尷尬,所以在啟用線程池的情況下,不建議使用ITL做值傳遞。為了解決這種問題,阿里做了transmittable-thread-local(TTL)來解決線程池異步值傳遞問題,下一篇,我們將會分析TTL的用法及原理。

?

轉載于:https://www.cnblogs.com/hama1993/p/10400265.html

總結

以上是生活随笔為你收集整理的ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。