<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連問(wèn)

          共 4872字,需瀏覽 10分鐘

           ·

          2022-05-17 08:01

          前言

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

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

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

          1. 為什么要用ThreadLocal?

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

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

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

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

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

          它的核心思想是:共享變量在每個(gè)線程都有一個(gè)副本,每個(gè)線程操作的都是自己的副本,對(duì)另外的線程沒(méi)有影響。

          例如:

          @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的底層實(shí)現(xiàn)原理,我們不得不扒一下源碼。

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

          public?class?ThreadLocal<T>?{
          ?????...
          ?????public?T?get()?{
          ????????//獲取當(dāng)前線程
          ????????Thread?t?=?Thread.currentThread();
          ????????//獲取當(dāng)前線程的成員變量ThreadLocalMap對(duì)象
          ????????ThreadLocalMap?map?=?getMap(t);
          ????????if?(map?!=?null)?{
          ????????????//根據(jù)threadLocal對(duì)象從map中獲取Entry對(duì)象
          ????????????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();
          ????????//獲取當(dāng)前線程
          ????????Thread?t?=?Thread.currentThread();
          ????????//獲取當(dāng)前線程的成員變量ThreadLocalMap對(duì)象
          ????????ThreadLocalMap?map?=?getMap(t);
          ????????//如果map不為空
          ????????if?(map?!=?null)
          ????????????//將初始值設(shè)置到map中,key是this,即threadLocal對(duì)象,value是初始值
          ????????????map.set(this,?value);
          ????????else
          ???????????//如果map為空,則需要?jiǎng)?chuàng)建新的map對(duì)象
          ????????????createMap(t,?value);
          ????????return?value;
          ????}
          ????
          ????public?void?set(T?value)?{
          ????????//獲取當(dāng)前線程
          ????????Thread?t?=?Thread.currentThread();
          ????????//獲取當(dāng)前線程的成員變量ThreadLocalMap對(duì)象
          ????????ThreadLocalMap?map?=?getMap(t);
          ????????//如果map不為空
          ????????if?(map?!=?null)
          ????????????//將值設(shè)置到map中,key是this,即threadLocal對(duì)象,value是傳入的value值
          ????????????map.set(this,?value);
          ????????else
          ???????????//如果map為空,則需要?jiǎng)?chuàng)建新的map對(duì)象
          ????????????createMap(t,?value);
          ????}
          ????
          ?????static?class?ThreadLocalMap?{
          ????????...
          ?????}
          ?????...
          }

          ThreadLocalget方法、set方法和setInitialValue方法,其實(shí)最終操作的都是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里面包含一個(gè)靜態(tài)的內(nèi)部類Entry,該類繼承于WeakReference類,說(shuō)明Entry是一個(gè)弱引用。

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

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

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

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

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

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

          需要特別說(shuō)明的是,上圖中ThreadLocal對(duì)象我畫到了堆上,其實(shí)在實(shí)際的業(yè)務(wù)場(chǎng)景中不一定在堆上。因?yàn)槿绻鸗hreadLocal被定義成了static的,ThreadLocal的對(duì)象是類共用的,可能出現(xiàn)在方法區(qū)。

          3. 為什么用ThreadLocal做key?

          不知道你有沒(méi)有思考過(guò)這樣一個(gè)問(wèn)題:ThreadLocalMap為什么要用ThreadLocal做key,而不是用Thread做key?

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

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

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

          @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時(shí),你的代碼中定義了3個(gè)ThreadLocal對(duì)象,那么,通過(guò)Thread對(duì)象,它怎么知道要獲取哪個(gè)ThreadLocal對(duì)象呢?

          如下圖所示:

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

          如下圖所示:

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

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

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

          假如key對(duì)ThreadLocal對(duì)象的弱引用,改為強(qiáng)引用。我們都知道ThreadLocal變量對(duì)ThreadLocal對(duì)象是有強(qiáng)引用存在的。

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

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

          就會(huì)存在這樣的強(qiáng)引用鏈:Thread變量 -> Thread對(duì)象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal對(duì)象。

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

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

          弱引用的對(duì)象,在GC做垃圾清理的時(shí)候,就會(huì)被自動(dòng)回收了。

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

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

          由于當(dāng)前的ThreadLocal變量已經(jīng)被指向null了,但如果直接調(diào)用它的getsetremove方法,很顯然會(huì)出現(xiàn)空指針異常。因?yàn)樗纳呀?jīng)結(jié)束了,再調(diào)用它的方法也沒(méi)啥意義。

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

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

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

          需要特別注意的地方是:

          1. key為null的條件是,ThreadLocal變量指向null,并且key是弱引用。如果ThreadLocal變量沒(méi)有斷開(kāi)對(duì)ThreadLocal的強(qiáng)引用,即ThreadLocal變量沒(méi)有指向null,GC就貿(mào)然的把弱引用的key回收了,不就會(huì)影響正常用戶的使用?
          2. 如果當(dāng)前ThreadLocal變量指向null了,并且key也為null了,但如果沒(méi)有其他ThreadLocal變量觸發(fā)getsetremove方法,也會(huì)造成內(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處理的對(duì)象,沒(méi)有其他引用,在調(diào)用gc方法后,弱引用對(duì)象會(huì)被自動(dòng)回收。

          但如果出現(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

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

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

          如果將object強(qiáng)引用設(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之后,弱引用能夠被正常回收。

          由此可見(jiàn),如果強(qiáng)引用和弱引用同時(shí)關(guān)聯(lián)一個(gè)對(duì)象,那么這個(gè)對(duì)象是不會(huì)被GC回收。也就是說(shuō)這種情況下Entry的key,一直都不會(huì)為null,除非強(qiáng)引用主動(dòng)斷開(kāi)關(guān)聯(lián)。

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

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

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

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

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

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

          答案是否定的。

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

          那么,Entry的value值一直都沒(méi)被清空。

          所以會(huì)存在這樣一條強(qiáng)引用鏈:Thread變量 -> Thread對(duì)象 -> ThreadLocalMap -> Entry -> value -> Object。

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

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

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

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

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

          先創(chuàng)建一個(gè)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方法清理沒(méi)用的數(shù)據(jù)。如果業(yè)務(wù)代碼出現(xiàn)異常,也能及時(shí)清理沒(méi)用的數(shù)據(jù)。

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

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

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

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

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

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

          通過(guò)key的hashCode值,數(shù)組的長(zhǎng)度減1。其中key就是ThreadLocal對(duì)象,數(shù)組的長(zhǎng)度減1,相當(dāng)于除以數(shù)組的長(zhǎng)度減1,然后取模

          這是一種hash算法。

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

          于是: int i = 31 & 15 = 15

          相當(dāng)于:int i = 31 % 16 = 15

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

          為什么與運(yùn)算效率更高?

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

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

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

          我們看看getEntry是怎么做的:

          private?Entry?getEntry(ThreadLocal?key)?{
          ????//通過(guò)hash算法獲取下標(biāo)值
          ????int?i?=?key.threadLocalHashCode?&?(table.length?-?1);
          ????Entry?e?=?table[i];
          ????//如果下標(biāo)位置上的key正好是我們所需要尋找的key
          ????if?(e?!=?null?&&?e.get()?==?key)
          ????????//說(shuō)明找到數(shù)據(jù)了,直接返回
          ????????return?e;
          ????else
          ????????//說(shuō)明出現(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對(duì)象如果不為空,則一直循環(huán)
          ????while?(e?!=?null)?{
          ????????ThreadLocal?k?=?e.get();
          ????????//如果當(dāng)前Entry的key正好是我們所需要尋找的key
          ????????if?(k?==?key)
          ????????????//說(shuō)明這次真的找到數(shù)據(jù)了
          ????????????return?e;
          ????????if?(k?==?null)
          ????????????//如果key為空,則清理臟數(shù)據(jù)
          ????????????expungeStaleEntry(i);
          ????????else
          ????????????//如果還是沒(méi)找到數(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);
          }

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

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

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

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

          8. ThreadLocal是如何擴(kuò)容的?

          從上面得知,ThreadLocal的初始大小是16。那么問(wèn)題來(lái)了,ThreadLocal是如何擴(kuò)容的?

          set方法中會(huì)調(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();
          }

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

          threshold默認(rèn)是0,在創(chuàng)建ThreadLocalMap時(shí),調(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è)置一個(gè)值,而這個(gè)值INITIAL_CAPACITY是默認(rèn)的大小16。

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

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

          換句話說(shuō)當(dāng)sz大于等于10時(shí),就可以考慮擴(kuò)容了。

          rehash代碼如下:

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

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

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

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

          計(jì)算公式如下:

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

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

          private?void?resize()?{
          ????Entry[]?oldTab?=?table;
          ????int?oldLen?=?oldTab.length;
          ????//按2倍的大小擴(kuò)容
          ????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倍的大小擴(kuò)容。

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

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

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

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

          但在實(shí)際工作中,有可能是在父子線程中共享數(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

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

          顯然通過(guò)ThreadLocal,無(wú)法在父子線程中共享數(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è)置的值。

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

          Thread類的部分代碼如下:

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

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

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

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

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

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

          因?yàn)樯婕暗讲煌木€程,如果直接使用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

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

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

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

          因?yàn)榈诙蝧ubmit任務(wù)的時(shí)候,線程池中已經(jīng)有一個(gè)線程了,就直接拿過(guò)來(lái)復(fù)用,不會(huì)再重新創(chuàng)建線程了。所以不會(huì)再調(diào)用線程的init方法,所以第二次其實(shí)沒(méi)有獲取到最新的數(shù)據(jù)7,還是獲取的老數(shù)據(jù)6。

          那么,這該怎么辦呢?

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

          可以通過(guò)如下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。

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

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

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

          以TtlRunnable類為例,它實(shí)現(xiàn)了Runnable接口,同時(shí)還實(shí)現(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. 把當(dāng)時(shí)的ThreadLocal做個(gè)備份,然后將父類的ThreadLocal拷貝過(guò)來(lái)。
          2. 執(zhí)行真正的run方法,可以獲取到父類最新的ThreadLocal數(shù)據(jù)。
          3. 從備份的數(shù)據(jù)中,恢復(fù)當(dāng)時(shí)的ThreadLocal數(shù)據(jù)。

          11. ThreadLocal有哪些用途?

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

          老實(shí)說(shuō),使用ThreadLocal的場(chǎng)景挺多的。

          下面列舉幾個(gè)常見(jiàn)的場(chǎng)景:

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

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

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

          接下來(lái)留幾個(gè)問(wèn)題給大家思考一下:

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

          敬請(qǐng)期待我的下一篇文章,謝謝。



          往期推薦

          下個(gè)十年高性能 JSON 庫(kù)來(lái)了:fastjson2!


          一文詳解讀寫鎖


          梳理50道經(jīng)典計(jì)算機(jī)網(wǎng)絡(luò)面試題


          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                    高圆圆一区二区三区 | 中文字幕操逼视频 | 日本精品黄页网 | 久久久久国际精品视频 | 亚洲午夜视频 |