<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ThreadLocal奪命11連問

          共 4995字,需瀏覽 10分鐘

           ·

          2022-06-08 15:56

          點擊關(guān)注公眾號,Java干貨及時送達

          前一段時間,有同事使用ThreadLocal踩坑了,正好引起了我的興趣。

          所以近期,我抽空把ThreadLocal的源碼再研究了一下,越看越有意思,發(fā)現(xiàn)里面的東西還真不少。

          我把精華濃縮了一下,匯集成了下面11個問題,看看你能頂住第幾個?

          1. 為什么要用ThreadLocal?

          并發(fā)編程是一項非常重要的技術(shù),它讓我們的程序變得更加高效。

          但在并發(fā)的場景中,如果有多個線程同時修改公共變量,可能會出現(xiàn)線程安全問題,即該變量最終結(jié)果可能出現(xiàn)異常。

          為了解決線程安全問題,JDK出現(xiàn)了很多技術(shù)手段,比如:使用synchronizedLock,給訪問公共資源的代碼上鎖,保證了代碼的原子性

          但在高并發(fā)的場景中,如果多個線程同時競爭一把鎖,這時會存在大量的鎖等待,可能會浪費很多時間,讓系統(tǒng)的響應(yīng)時間一下子變慢。

          因此,JDK還提供了另外一種用空間換時間的新思路:ThreadLocal

          它的核心思想是:共享變量在每個線程都有一個副本,每個線程操作的都是自己的副本,對另外的線程沒有影響。

          例如:

          @Service
          public?class?ThreadLocalService?{
          ????private?static?final?ThreadLocal?threadLocal?=?new?ThreadLocal<>();

          ????public?void?add()?{
          ????????threadLocal.set(1);
          ????????doSamething();
          ????????Integer?integer?=?threadLocal.get();
          ????}
          }

          2. ThreadLocal的原理是什么?

          為了搞清楚ThreadLocal的底層實現(xiàn)原理,我們不得不扒一下源碼。

          ThreadLocal的內(nèi)部有一個靜態(tài)的內(nèi)部類叫:ThreadLocalMap

          public?class?ThreadLocal<T>?{
          ?????...
          ?????public?T?get()?{
          ????????//獲取當前線程
          ????????Thread?t?=?Thread.currentThread();
          ????????//獲取當前線程的成員變量ThreadLocalMap對象
          ????????ThreadLocalMap?map?=?getMap(t);
          ????????if?(map?!=?null)?{
          ????????????//根據(jù)threadLocal對象從map中獲取Entry對象
          ????????????ThreadLocalMap.Entry?e?=?map.getEntry(this);
          ????????????if?(e?!=?null)?{
          ????????????????@SuppressWarnings("unchecked")
          ????????????????//獲取保存的數(shù)據(jù)
          ????????????????T?result?=?(T)e.value;
          ????????????????return?result;
          ????????????}
          ????????}
          ????????//初始化數(shù)據(jù)
          ????????return?setInitialValue();
          ????}
          ????
          ????private?T?setInitialValue()?{
          ????????//獲取要初始化的數(shù)據(jù)
          ????????T?value?=?initialValue();
          ????????//獲取當前線程
          ????????Thread?t?=?Thread.currentThread();
          ????????//獲取當前線程的成員變量ThreadLocalMap對象
          ????????ThreadLocalMap?map?=?getMap(t);
          ????????//如果map不為空
          ????????if?(map?!=?null)
          ????????????//將初始值設(shè)置到map中,key是this,即threadLocal對象,value是初始值
          ????????????map.set(this,?value);
          ????????else
          ???????????//如果map為空,則需要創(chuàng)建新的map對象
          ????????????createMap(t,?value);
          ????????return?value;
          ????}
          ????
          ????public?void?set(T?value)?{
          ????????//獲取當前線程
          ????????Thread?t?=?Thread.currentThread();
          ????????//獲取當前線程的成員變量ThreadLocalMap對象
          ????????ThreadLocalMap?map?=?getMap(t);
          ????????//如果map不為空
          ????????if?(map?!=?null)
          ????????????//將值設(shè)置到map中,key是this,即threadLocal對象,value是傳入的value值
          ????????????map.set(this,?value);
          ????????else
          ???????????//如果map為空,則需要創(chuàng)建新的map對象
          ????????????createMap(t,?value);
          ????}
          ????
          ?????static?class?ThreadLocalMap?{
          ????????...
          ?????}
          ?????...
          }

          ThreadLocalget方法、set方法和setInitialValue方法,其實最終操作的都是ThreadLocalMap類中的數(shù)據(jù)。

          其中ThreadLocalMap類的內(nèi)部如下:

          static?class?ThreadLocalMap?{
          ????static?class?Entry?extends?WeakReference<ThreadLocal>?{
          ????????Object?value;

          ????????Entry(ThreadLocal?k,?Object?v)?{
          ????????????super(k);
          ????????????value?=?v;
          ????????}
          ???}
          ???...
          ???private?Entry[]?table;
          ???...
          }

          ThreadLocalMap里面包含一個靜態(tài)的內(nèi)部類Entry,該類繼承于WeakReference類,說明Entry是一個弱引用。

          ThreadLocalMap內(nèi)部還包含了一個Entry數(shù)組,其中:Entry = ThreadLocal + value

          ThreadLocalMap被定義成了Thread類的成員變量。

          public?class?Thread?implements?Runnable?{
          ????...
          ????ThreadLocal.ThreadLocalMap?threadLocals?=?null;
          }

          下面用一張圖從宏觀上,認識一下ThreadLocal的整體結(jié)構(gòu):從上圖中看出,在每個Thread類中,都有一個ThreadLocalMap的成員變量,該變量包含了一個Entry數(shù)組,該數(shù)組真正保存了ThreadLocal類set的數(shù)據(jù)。

          Entry是由threadLocal和value組成,其中threadLocal對象是弱引用,在GC的時候,會被自動回收。而value就是ThreadLocal類set的數(shù)據(jù)。

          下面用一張圖總結(jié)一下引用關(guān)系:上圖中除了Entry的key對ThreadLocal對象是弱引用,其他的引用都是強引用

          需要特別說明的是,上圖中ThreadLocal對象我畫到了堆上,其實在實際的業(yè)務(wù)場景中不一定在堆上。因為如果ThreadLocal被定義成了static的,ThreadLocal的對象是類共用的,可能出現(xiàn)在方法區(qū)。

          3. 為什么用ThreadLocal做key?

          不知道你有沒有思考過這樣一個問題:ThreadLocalMap為什么要用ThreadLocal做key,而不是用Thread做key?

          如果在你的應(yīng)用中,一個線程中只使用了一個ThreadLocal對象,那么使用Thread做key也未嘗不可。

          @Service
          public?class?ThreadLocalService?{
          ????private?static?final?ThreadLocal?threadLocal?=?new?ThreadLocal<>();
          }????

          但實際情況中,你的應(yīng)用,一個線程中很有可能不只使用了一個ThreadLocal對象。這時使用Thread做key不就出有問題?

          @Service
          public?class?ThreadLocalService?{
          ????private?static?final?ThreadLocal?threadLocal1?=?new?ThreadLocal<>();
          ????private?static?final?ThreadLocal?threadLocal2?=?new?ThreadLocal<>();
          ????private?static?final?ThreadLocal?threadLocal3?=?new?ThreadLocal<>();
          }????

          假如使用Thread做key時,你的代碼中定義了3個ThreadLocal對象,那么,通過Thread對象,它怎么知道要獲取哪個ThreadLocal對象呢?

          如下圖所示:

          因此,不能使用Thread做key,而應(yīng)該改成用ThreadLocal對象做key,這樣才能通過具體ThreadLocal對象的get方法,輕松獲取到你想要的ThreadLocal對象。

          如下圖所示:

          4. Entry的key為什么設(shè)計成弱引用?

          前面說過,Entry的key,傳入的是ThreadLocal對象,使用了WeakReference對象,即被設(shè)計成了弱引用。

          那么,為什么要這樣設(shè)計呢?

          假如key對ThreadLocal對象的弱引用,改為強引用。我們都知道ThreadLocal變量對ThreadLocal對象是有強引用存在的。

          即使ThreadLocal變量生命周期完了,設(shè)置成null了,但由于key對ThreadLocal還是強引用。

          此時,如果執(zhí)行該代碼的線程使用了線程池,一直長期存在,不會被銷毀。

          就會存在這樣的強引用鏈:Thread變量 -> Thread對象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal對象。

          那么,ThreadLocal對象和ThreadLocalMap都將不會被GC回收,于是產(chǎn)生了內(nèi)存泄露問題。

          為了解決這個問題,JDK的開發(fā)者們把Entry的key設(shè)計成了弱引用

          弱引用的對象,在GC做垃圾清理的時候,就會被自動回收了。

          如果key是弱引用,當ThreadLocal變量指向null之后,在GC做垃圾清理的時候,key會被自動回收,其值也被設(shè)置成null。

          如下圖所示:接下來,最關(guān)鍵的地方來了。

          由于當前的ThreadLocal變量已經(jīng)被指向null了,但如果直接調(diào)用它的getsetremove方法,很顯然會出現(xiàn)空指針異常。因為它的生命已經(jīng)結(jié)束了,再調(diào)用它的方法也沒啥意義。

          此時,如果系統(tǒng)中還定義了另外一個ThreadLocal變量b,調(diào)用了它的getsetremove,三個方法中的任何一個方法,都會自動觸發(fā)清理機制,將key為null的value值清空。

          如果key和value都是null,那么Entry對象會被GC回收。如果所有的Entry對象都被回收了,ThreadLocalMap也會被回收了。

          這樣就能最大程度的解決內(nèi)存泄露問題。

          需要特別注意的地方是:

          1. key為null的條件是,ThreadLocal變量指向null,并且key是弱引用。如果ThreadLocal變量沒有斷開對ThreadLocal的強引用,即ThreadLocal變量沒有指向null,GC就貿(mào)然的把弱引用的key回收了,不就會影響正常用戶的使用?
          2. 如果當前ThreadLocal變量指向null了,并且key也為null了,但如果沒有其他ThreadLocal變量觸發(fā)getsetremove方法,也會造成內(nèi)存泄露。

          下面看看弱引用的例子:

          public?static?void?main(String[]?args)?{
          ????WeakReference?weakReference0?=?new?WeakReference<>(new?Object());
          ????System.out.println(weakReference0.get());
          ????System.gc();
          ????System.out.println(weakReference0.get());
          }

          打印結(jié)果:

          java.lang.Object@1ef7fe8e
          null

          傳入WeakReference構(gòu)造方法的是直接new處理的對象,沒有其他引用,在調(diào)用gc方法后,弱引用對象會被自動回收。

          但如果出現(xiàn)下面這種情況:

          public?static?void?main(String[]?args)?{
          ????Object?object?=?new?Object();
          ????WeakReference?weakReference1?=?new?WeakReference<>(object);
          ????System.out.println(weakReference1.get());
          ????System.gc();
          ????System.out.println(weakReference1.get());
          }

          執(zhí)行結(jié)果:

          java.lang.Object@1ef7fe8e
          java.lang.Object@1ef7fe8e

          先定義了一個強引用object對象,在WeakReference構(gòu)造方法中將object對象的引用作為參數(shù)傳入。這時,調(diào)用gc后,弱引用對象不會被自動回收。

          我們的Entry對象中的key不就是第二種情況嗎?在Entry構(gòu)造方法中傳入的是ThreadLocal對象的引用。

          如果將object強引用設(shè)置為null:

          public?static?void?main(String[]?args)?{
          ????Object?object?=?new?Object();
          ????WeakReference?weakReference1?=?new?WeakReference<>(object);
          ????System.out.println(weakReference1.get());
          ????System.gc();
          ????System.out.println(weakReference1.get());

          ????object=null;
          ????System.gc();
          ????System.out.println(weakReference1.get());
          }

          執(zhí)行結(jié)果:

          java.lang.Object@6f496d9f
          java.lang.Object@6f496d9f
          null

          第二次gc之后,弱引用能夠被正常回收。

          由此可見,如果強引用和弱引用同時關(guān)聯(lián)一個對象,那么這個對象是不會被GC回收。也就是說這種情況下Entry的key,一直都不會為null,除非強引用主動斷開關(guān)聯(lián)。

          此外,你可能還會問這樣一個問題:Entry的value為什么不設(shè)計成弱引用?

          答:Entry的value假如只是被Entry引用,有可能沒被業(yè)務(wù)系統(tǒng)中的其他地方引用。如果將value改成了弱引用,被GC貿(mào)然回收了(數(shù)據(jù)突然沒了),可能會導(dǎo)致業(yè)務(wù)系統(tǒng)出現(xiàn)異常。

          而相比之下,Entry的key,管理的地方就非常明確了。

          這就是Entry的key被設(shè)計成弱引用,而value沒被設(shè)計成弱引用的原因。

          5. ThreadLocal真的會導(dǎo)致內(nèi)存泄露?

          通過上面的Entry對象中的key設(shè)置成弱引用,并且使用getsetremove方法清理key為null的value值,就能徹底解決內(nèi)存泄露問題?

          答案是否定的。

          如下圖所示:假如ThreadLocalMap中存在很多key為null的Entry,但后面的程序,一直都沒有調(diào)用過有效的ThreadLocal的getsetremove方法。

          那么,Entry的value值一直都沒被清空。

          所以會存在這樣一條強引用鏈:Thread變量 -> Thread對象 -> ThreadLocalMap -> Entry -> value -> Object。

          其結(jié)果就是:Entry和ThreadLocalMap將會長期存在下去,會導(dǎo)致內(nèi)存泄露

          6. 如何解決內(nèi)存泄露問題?

          前面說過的ThreadLocal還是會導(dǎo)致內(nèi)存泄露的問題,我們有沒有解決辦法呢?

          答:有辦法,調(diào)用ThreadLocal對象的remove方法。

          不是在一開始就調(diào)用remove方法,而是在使用完ThreadLocal對象之后。列如:

          先創(chuàng)建一個CurrentUser類,其中包含了ThreadLocal的邏輯。

          public?class?CurrentUser?{
          ????private?static?final?ThreadLocal?THREA_LOCAL?=?new?ThreadLocal();
          ????
          ????public?static?void?set(UserInfo?userInfo)?{
          ????????THREA_LOCAL.set(userInfo);
          ????}
          ????
          ????public?static?UserInfo?get()?{
          ???????THREA_LOCAL.get();
          ????}
          ????
          ????public?static?void?remove()?{
          ???????THREA_LOCAL.remove();
          ????}
          }

          然后在業(yè)務(wù)代碼中調(diào)用相關(guān)方法:

          public?void?doSamething(UserDto?userDto)?{
          ???UserInfo?userInfo?=?convert(userDto);
          ???
          ???try{
          ?????CurrentUser.set(userInfo);
          ?????...
          ?????
          ?????//業(yè)務(wù)代碼
          ?????UserInfo?userInfo?=?CurrentUser.get();
          ?????...
          ???}?finally?{
          ??????CurrentUser.remove();
          ???}
          }

          需要我們特別注意的地方是:一定要在finally代碼塊中,調(diào)用remove方法清理沒用的數(shù)據(jù)。如果業(yè)務(wù)代碼出現(xiàn)異常,也能及時清理沒用的數(shù)據(jù)。

          remove方法中會把Entry中的key和value都設(shè)置成null,這樣就能被GC及時回收,無需觸發(fā)額外的清理機制,所以它能解決內(nèi)存泄露問題。

          7. ThreadLocal是如何定位數(shù)據(jù)的?

          前面說過ThreadLocalMap對象底層是用Entry數(shù)組保存數(shù)據(jù)的。

          那么問題來了,ThreadLocal是如何定位Entry數(shù)組數(shù)據(jù)的?

          在ThreadLocal的get、set、remove方法中都有這樣一行代碼:

          int?i?=?key.threadLocalHashCode?&?(len-1);

          通過key的hashCode值,數(shù)組的長度減1。其中key就是ThreadLocal對象,數(shù)組的長度減1,相當于除以數(shù)組的長度減1,然后取模

          這是一種hash算法。

          接下來給大家舉個例子:假設(shè)len=16,key.threadLocalHashCode=31,

          于是:int i = 31 & 15 = 15

          相當于:int i = 31 % 16 = 15

          計算的結(jié)果是一樣的,但是使用與運算效率跟高一些。

          為什么與運算效率更高?

          答:因為ThreadLocal的初始大小是16,每次都是按2倍擴容,數(shù)組的大小其實一直都是2的n次方。這種數(shù)據(jù)有個規(guī)律就是高位是0,低位都是1。在做與運算時,可以不用考慮高位,因為與運算的結(jié)果必定是0。只需考慮低位的與運算,所以效率更高。

          如果使用hash算法定位具體位置的話,就可能會出現(xiàn)hash沖突的情況,即兩個不同的hashCode取模后的值相同。

          ThreadLocal是如何解決hash沖突的呢?

          我們看看getEntry是怎么做的:

          private?Entry?getEntry(ThreadLocal?key)?{
          ????//通過hash算法獲取下標值
          ????int?i?=?key.threadLocalHashCode?&?(table.length?-?1);
          ????Entry?e?=?table[i];
          ????//如果下標位置上的key正好是我們所需要尋找的key
          ????if?(e?!=?null?&&?e.get()?==?key)
          ????????//說明找到數(shù)據(jù)了,直接返回
          ????????return?e;
          ????else
          ????????//說明出現(xiàn)hash沖突了,繼續(xù)往后找
          ????????return?getEntryAfterMiss(key,?i,?e);
          }

          再看看getEntryAfterMiss方法:

          private?Entry?getEntryAfterMiss(ThreadLocal?key,?int?i,?Entry?e)?{
          ????Entry[]?tab?=?table;
          ????int?len?=?tab.length;

          ????//判斷Entry對象如果不為空,則一直循環(huán)
          ????while?(e?!=?null)?{
          ????????ThreadLocal?k?=?e.get();
          ????????//如果當前Entry的key正好是我們所需要尋找的key
          ????????if?(k?==?key)
          ????????????//說明這次真的找到數(shù)據(jù)了
          ????????????return?e;
          ????????if?(k?==?null)
          ????????????//如果key為空,則清理臟數(shù)據(jù)
          ????????????expungeStaleEntry(i);
          ????????else
          ????????????//如果還是沒找到數(shù)據(jù),則繼續(xù)往后找
          ????????????i?=?nextIndex(i,?len);
          ????????e?=?tab[i];
          ????}
          ????return?null;
          }

          關(guān)鍵看看nextIndex方法:

          private?static?int?nextIndex(int?i,?int?len)?{
          ????return?((i?+?1?1?:?0);
          }

          當通過hash算法計算出的下標小于數(shù)組大小,則將下標值加1。否則,即下標大于等于數(shù)組大小,下標變成0了。下標變成0之后,則循環(huán)一次,下標又變成1。。。

          尋找的大致過程如下圖所示:如果找到最后一個,還是沒有找到,則再從頭開始找。不知道你有沒有發(fā)現(xiàn),它構(gòu)成了一個:環(huán)形

          ThreadLocal從數(shù)組中找數(shù)據(jù)的過程大致是這樣的:

          1. 通過key的hashCode取余計算出一個下標。
          2. 通過下標,在數(shù)組中定位具體Entry,如果key正好是我們所需要的key,說明找到了,則直接返回數(shù)據(jù)。
          3. 如果第2步?jīng)]有找到我們想要的數(shù)據(jù),則從數(shù)組的下標位置,繼續(xù)往后面找。
          4. 如果第3步中找key的正好是我們所需要的key,說明找到了,則直接返回數(shù)據(jù)。
          5. 如果還是沒有找到數(shù)據(jù),再繼續(xù)往后面找。如果找到最后一個位置,還是沒有找到數(shù)據(jù),則再從頭,即下標為0的位置,繼續(xù)從前往后找數(shù)據(jù)。
          6. 直到找到第一個Entry為空為止。

          8. ThreadLocal是如何擴容的?

          從上面得知,ThreadLocal的初始大小是16。那么問題來了,ThreadLocal是如何擴容的?

          set方法中會調(diào)用rehash方法:

          private?void?set(ThreadLocal?key,?Object?value)?{
          ????Entry[]?tab?=?table;
          ????int?len?=?tab.length;
          ????int?i?=?key.threadLocalHashCode?&?(len-1);

          ????for?(Entry?e?=?tab[i];
          ?????????e?!=?null;
          ?????????e?=?tab[i?=?nextIndex(i,?len)])?{
          ????????ThreadLocal?k?=?e.get();

          ????????if?(k?==?key)?{
          ????????????e.value?=?value;
          ????????????return;
          ????????}

          ????????if?(k?==?null)?{
          ????????????replaceStaleEntry(key,?value,?i);
          ????????????return;
          ????????}
          ????}

          ????tab[i]?=?new?Entry(key,?value);
          ????int?sz?=?++size;
          ????if?(!cleanSomeSlots(i,?sz)?&&?sz?>=?threshold)
          ????????rehash();
          }

          注意一下,其中有個判斷條件是:sz(之前的size+1)如果大于或等于threshold的話,則調(diào)用rehash方法。

          threshold默認是0,在創(chuàng)建ThreadLocalMap時,調(diào)用它的構(gòu)造方法:

          ThreadLocalMap(ThreadLocal?firstKey,?Object?firstValue)?{
          ????table?=?new?Entry[INITIAL_CAPACITY];
          ????int?i?=?firstKey.threadLocalHashCode?&?(INITIAL_CAPACITY?-?1);
          ????table[i]?=?new?Entry(firstKey,?firstValue);
          ????size?=?1;
          ????setThreshold(INITIAL_CAPACITY);
          }

          調(diào)用setThreshold方法給threshold設(shè)置一個值,而這個值INITIAL_CAPACITY是默認的大小16。

          private?void?setThreshold(int?len)?{
          ????threshold?=?len?*?2?/?3;
          }

          也就是第一次設(shè)置的threshold = 16 * 2 / 3, 取整后的值是:10。

          換句話說當sz大于等于10時,就可以考慮擴容了。

          rehash代碼如下:

          private?void?rehash()?{
          ????//先嘗試回收一次key為null的值,騰出一些空間
          ????expungeStaleEntries();

          ????if?(size?>=?threshold?-?threshold?/?4)
          ????????resize();
          }

          在真正擴容之前,先嘗試回收一次key為null的值,騰出一些空間。

          如果回收之后的size大于等于threshold的3/4時,才需要真正的擴容。

          計算公式如下:

          16?*?2?*?4?/?3?*?4?-?16?*?2?/?3?*?4?=?8

          也就是說添加數(shù)據(jù)后,新的size大于等于老size的1/2時,才需要擴容。

          private?void?resize()?{
          ????Entry[]?oldTab?=?table;
          ????int?oldLen?=?oldTab.length;
          ????//按2倍的大小擴容
          ????int?newLen?=?oldLen?*?2;
          ????Entry[]?newTab?=?new?Entry[newLen];
          ????int?count?=?0;

          ????for?(int?j?=?0;?j?????????Entry?e?=?oldTab[j];
          ????????if?(e?!=?null)?{
          ????????????ThreadLocal?k?=?e.get();
          ????????????if?(k?==?null)?{
          ????????????????e.value?=?null;?//?Help?the?GC
          ????????????}?else?{
          ????????????????int?h?=?k.threadLocalHashCode?&?(newLen?-?1);
          ????????????????while?(newTab[h]?!=?null)
          ????????????????????h?=?nextIndex(h,?newLen);
          ????????????????newTab[h]?=?e;
          ????????????????count++;
          ????????????}
          ????????}
          ????}

          ????setThreshold(newLen);
          ????size?=?count;
          ????table?=?newTab;
          }

          resize中每次都是按2倍的大小擴容。

          擴容的過程如下圖所示:擴容的關(guān)鍵步驟如下:

          1. 老size + 1 = 新size
          2. 如果新size大于等于老size的2/3時,需要考慮擴容。
          3. 擴容前先嘗試回收一次key為null的值,騰出一些空間。
          4. 如果回收之后發(fā)現(xiàn)size還是大于等于老size的1/2時,才需要真正的擴容。
          5. 每次都是按2倍的大小擴容。

          9. 父子線程如何共享數(shù)據(jù)?

          前面介紹的ThreadLocal都是在一個線程中保存和獲取數(shù)據(jù)的。

          但在實際工作中,有可能是在父子線程中共享數(shù)據(jù)的。即在父線程中往ThreadLocal設(shè)置了值,在子線程中能夠獲取到。

          例如:

          public?class?ThreadLocalTest?{

          ????public?static?void?main(String[]?args)?{
          ????????ThreadLocal?threadLocal?=?new?ThreadLocal<>();
          ????????threadLocal.set(6);
          ????????System.out.println("父線程獲取數(shù)據(jù):"?+?threadLocal.get());

          ????????new?Thread(()?->?{
          ????????????System.out.println("子線程獲取數(shù)據(jù):"?+?threadLocal.get());
          ????????}).start();
          ????}
          }

          執(zhí)行結(jié)果:

          父線程獲取數(shù)據(jù):6
          子線程獲取數(shù)據(jù):null

          你會發(fā)現(xiàn),在這種情況下使用ThreadLocal是行不通的。main方法是在主線程中執(zhí)行的,相當于父線程。在main方法中開啟了另外一個線程,相當于子線程。

          顯然通過ThreadLocal,無法在父子線程中共享數(shù)據(jù)。

          那么,該怎么辦呢?

          答:使用InheritableThreadLocal,它是JDK自帶的類,繼承了ThreadLocal類。

          修改代碼之后:

          public?class?ThreadLocalTest?{

          ????public?static?void?main(String[]?args)?{
          ????????InheritableThreadLocal?threadLocal?=?new?InheritableThreadLocal<>();
          ????????threadLocal.set(6);
          ????????System.out.println("父線程獲取數(shù)據(jù):"?+?threadLocal.get());

          ????????new?Thread(()?->?{
          ????????????System.out.println("子線程獲取數(shù)據(jù):"?+?threadLocal.get());
          ????????}).start();
          ????}
          }

          執(zhí)行結(jié)果:

          父線程獲取數(shù)據(jù):6
          子線程獲取數(shù)據(jù):6

          果然,在換成InheritableThreadLocal之后,在子線程中能夠正常獲取父線程中設(shè)置的值。

          其實,在Thread類中除了成員變量threadLocals之外,還有另一個成員變量:inheritableThreadLocals。

          Thread類的部分代碼如下:

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

          最關(guān)鍵的一點是,在它的init方法中會將父線程中往ThreadLocal設(shè)置的值,拷貝一份到子線程中。

          感興趣的小伙伴,可以找我私聊。或者看看我后面的文章,后面還會有專欄。

          10. 線程池中如何共享數(shù)據(jù)?

          在真實的業(yè)務(wù)場景中,一般很少用單獨的線程,絕大多數(shù),都是用的線程池

          那么,在線程池中如何共享ThreadLocal對象生成的數(shù)據(jù)呢?

          因為涉及到不同的線程,如果直接使用ThreadLocal,顯然是不合適的。

          我們應(yīng)該使用InheritableThreadLocal,具體代碼如下:

          private?static?void?fun1()?{
          ????InheritableThreadLocal?threadLocal?=?new?InheritableThreadLocal<>();
          ????threadLocal.set(6);
          ????System.out.println("父線程獲取數(shù)據(jù):"?+?threadLocal.get());

          ????ExecutorService?executorService?=?Executors.newSingleThreadExecutor();

          ????threadLocal.set(6);
          ????executorService.submit(()?->?{
          ????????System.out.println("第一次從線程池中獲取數(shù)據(jù):"?+?threadLocal.get());
          ????});

          ????threadLocal.set(7);
          ????executorService.submit(()?->?{
          ????????System.out.println("第二次從線程池中獲取數(shù)據(jù):"?+?threadLocal.get());
          ????});
          }

          執(zhí)行結(jié)果:

          父線程獲取數(shù)據(jù):6
          第一次從線程池中獲取數(shù)據(jù):6
          第二次從線程池中獲取數(shù)據(jù):6

          由于這個例子中使用了單例線程池,固定線程數(shù)是1。

          第一次submit任務(wù)的時候,該線程池會自動創(chuàng)建一個線程。因為使用了InheritableThreadLocal,所以創(chuàng)建線程時,會調(diào)用它的init方法,將父線程中的inheritableThreadLocals數(shù)據(jù)復(fù)制到子線程中。所以我們看到,在主線程中將數(shù)據(jù)設(shè)置成6,第一次從線程池中獲取了正確的數(shù)據(jù)6。

          之后,在主線程中又將數(shù)據(jù)改成7,但在第二次從線程池中獲取數(shù)據(jù)卻依然是6。

          因為第二次submit任務(wù)的時候,線程池中已經(jīng)有一個線程了,就直接拿過來復(fù)用,不會再重新創(chuàng)建線程了。所以不會再調(diào)用線程的init方法,所以第二次其實沒有獲取到最新的數(shù)據(jù)7,還是獲取的老數(shù)據(jù)6。

          那么,這該怎么辦呢?

          答:使用TransmittableThreadLocal,它并非JDK自帶的類,而是阿里巴巴開源jar包中的類。

          可以通過如下pom文件引入該jar包:

          <dependency>
          ???<groupId>com.alibabagroupId>
          ???<artifactId>transmittable-thread-localartifactId>
          ???<version>2.11.0version>
          ???<scope>compilescope>
          dependency>

          代碼調(diào)整如下:

          private?static?void?fun2()?throws?Exception?{
          ????TransmittableThreadLocal?threadLocal?=?new?TransmittableThreadLocal<>();
          ????threadLocal.set(6);
          ????System.out.println("父線程獲取數(shù)據(jù):"?+?threadLocal.get());

          ????ExecutorService?ttlExecutorService?=?TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

          ????threadLocal.set(6);
          ????ttlExecutorService.submit(()?->?{
          ????????System.out.println("第一次從線程池中獲取數(shù)據(jù):"?+?threadLocal.get());
          ????});

          ????threadLocal.set(7);
          ????ttlExecutorService.submit(()?->?{
          ????????System.out.println("第二次從線程池中獲取數(shù)據(jù):"?+?threadLocal.get());
          ????});

          }

          執(zhí)行結(jié)果:

          父線程獲取數(shù)據(jù):6
          第一次從線程池中獲取數(shù)據(jù):6
          第二次從線程池中獲取數(shù)據(jù):7

          我們看到,使用了TransmittableThreadLocal之后,第二次從線程中也能正確獲取最新的數(shù)據(jù)7了。

          nice。

          如果你仔細觀察這個例子,你可能會發(fā)現(xiàn),代碼中除了使用TransmittableThreadLocal類之外,還使用了TtlExecutors.getTtlExecutorService方法,去創(chuàng)建ExecutorService對象。

          這是非常重要的地方,如果沒有這一步,TransmittableThreadLocal在線程池中共享數(shù)據(jù)將不會起作用。

          創(chuàng)建ExecutorService對象,底層的submit方法會TtlRunnableTtlCallable對象。

          以TtlRunnable類為例,它實現(xiàn)了Runnable接口,同時還實現(xiàn)了它的run方法:

          public?void?run()?{
          ????Map,?Object>?copied?=?(Map)this.copiedRef.get();
          ????if?(copied?!=?null?&&?(!this.releaseTtlValueReferenceAfterRun?||?this.copiedRef.compareAndSet(copied,?(Object)null)))?{
          ????????Map?backup?=?TransmittableThreadLocal.backupAndSetToCopied(copied);

          ????????try?{
          ????????????this.runnable.run();
          ????????}?finally?{
          ????????????TransmittableThreadLocal.restoreBackup(backup);
          ????????}
          ????}?else?{
          ????????throw?new?IllegalStateException("TTL?value?reference?is?released?after?run!");
          ????}
          }

          這段代碼的主要邏輯如下:

          1. 把當時的ThreadLocal做個備份,然后將父類的ThreadLocal拷貝過來。
          2. 執(zhí)行真正的run方法,可以獲取到父類最新的ThreadLocal數(shù)據(jù)。
          3. 從備份的數(shù)據(jù)中,恢復(fù)當時的ThreadLocal數(shù)據(jù)。

          11. ThreadLocal有哪些用途?

          最后,一起聊聊ThreadLocal有哪些用途?

          老實說,使用ThreadLocal的場景挺多的。

          下面列舉幾個常見的場景:

          1. 在spring事務(wù)中,保證一個線程下,一個事務(wù)的多個操作拿到的是一個Connection。
          2. 在hiberate中管理session。
          3. 在JDK8之前,為了解決SimpleDateFormat的線程安全問題。
          4. 獲取當前登錄用戶上下文。
          5. 臨時保存權(quán)限數(shù)據(jù)。
          6. 使用MDC保存日志信息。

          等等,還有很多業(yè)務(wù)場景,這里就不一一列舉了。

          由于篇幅有限,今天的內(nèi)容先分享到這里。希望你看了這篇文章,會有所收獲。

          接下來留幾個問題給大家思考一下:

          1. ThreadLocal變量為什么建議要定義成static的?
          2. Entry數(shù)組為什么要通過hash算法計算下標,即直線尋址法,而不直接使用下標值?
          3. 強引用和弱引用有什么區(qū)別?
          4. Entry數(shù)組大小,為什么是2的N次方?
          5. 使用InheritableThreadLocal時,如果父線程中重新set值,在子線程中能夠正確的獲取修改后的新值嗎?

          ????

          1、拖動文件就能觸發(fā)7-Zip安全漏洞,波及所有版本

          2、進程切換的本質(zhì)是什么?

          3、一次 SQL 查詢優(yōu)化原理分析:900W+ 數(shù)據(jù),從 17s 到 300ms

          4、Redis數(shù)據(jù)結(jié)構(gòu)為什么既省內(nèi)存又高效?

          5、IntelliJ IDEA快捷鍵大全 + 動圖演示

          6、全球第三瀏覽器,封殺中國用戶這種操作!(文末送書)

          瀏覽 34
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    九一福利视频 | 国产精品婷婷久久久 | 一区二区三区四区无码免费 | 亚洲AV无码成人精品一区 | 日本的一级黄色片 |