<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 你真的會用嗎?

          共 12779字,需瀏覽 26分鐘

           ·

          2022-10-20 16:32

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

          d68594b22c4792a8084d2cb3e072c87f.webp來源:juejin.cn/post/6968022754096807966

          • ThreadLocal的作用以及應(yīng)用場景
          • 使用場景
          • 原理分析
          • ThreadLocalMap的底層結(jié)構(gòu)
          • 內(nèi)存泄露產(chǎn)生的原因
          • 解決Hash沖突
          • 使用ThreadLocal時對象存在哪里?

          ThreadLocal的作用以及應(yīng)用場景

          ThreadLocal算是一種并發(fā)容器吧,因為他的內(nèi)部是有ThreadLocalMap組成,ThreadLocal是為了解決多線程情況下變量不能被共享的問題,也就是多線程共享變量的問題。

          ThreadLocalLock以及Synchronized的區(qū)別是:ThreadLocal是給每個線程分配一個變量(對象),各個線程都存有變量的副本,這樣每個線程都是使用自己(變量)對象實例,使線程與線程之間進(jìn)行隔離;而LockSynchronized的方式是使線程有順序的執(zhí)行。

          舉一個簡單的例子:目前有100個學(xué)生等待簽字,但是老師只有一個筆,那老師只能按順序的分給每個學(xué)生,等待A學(xué)生簽字完成然后將筆交給B學(xué)生,這就類似LockSynchronized的方式。而ThreadLocal是,老師直接拿出一百個筆給每個學(xué)生;再效率提高的同事也要付出一個內(nèi)存消耗;也就是以空間換時間的概念

          使用場景

          Spring的事務(wù)隔離就是使用ThreadLocal和AOP來解決的;主要是TransactionSynchronizationManager這個類;

          解決SimpleDateFormat線程不安全問題;

          當(dāng)我們使用SimpleDateFormatparse()方法的時候,parse()方法會先調(diào)用Calendar.clear()方法,然后調(diào)用Calendar.add()方法,如果一個線程先調(diào)用了add()方法,然后另一個線程調(diào)用了clear()方法;這時候parse()方法就會出現(xiàn)解析錯誤;如果不信我們可以來個例子:

                
                public?class?SimpleDateFormatTest?{

          ????private?static?SimpleDateFormat?simpleDateFormat?=?new?SimpleDateFormat("yyyy-MM-dd");

          ????public?static?void?main(String[]?args)?{
          ????????for?(int?i?=?0;?i?<?50;?i++)?{
          ????????????Thread?thread?=?new?Thread(new?Runnable()?{
          ????????????????@Override
          ????????????????public?void?run()?{
          ????????????????????dateFormat();
          ????????????????}
          ????????????});
          ????????????thread.start();
          ????????}
          ????}

          ????/**
          ?????*?字符串轉(zhuǎn)成日期類型
          ?????*/
          ????public?static?void?dateFormat()?{
          ????????try?{
          ????????????simpleDateFormat.parse("2021-5-27");
          ????????}?catch?(ParseException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????}
          }

          這里我們只啟動了50個線程問題就會出現(xiàn),其實看巧不巧,有時候只有10個線程的情況就會出錯:

                
                Exception?in?thread?"Thread-40"?java.lang.NumberFormatException:?For?input?string:?""
          ?at?java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
          ?at?java.lang.Long.parseLong(Long.java:601)
          ?at?java.lang.Long.parseLong(Long.java:631)
          ?at?java.text.DigitList.getLong(DigitList.java:195)
          ?at?java.text.DecimalFormat.parse(DecimalFormat.java:2084)
          ?at?java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
          ?at?java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
          ?at?java.text.DateFormat.parse(DateFormat.java:364)
          ?at?cn.haoxy.use.lock.sdf.SimpleDateFormatTest.dateFormat(SimpleDateFormatTest.java:36)
          ?at?cn.haoxy.use.lock.sdf.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:23)
          ?at?java.lang.Thread.run(Thread.java:748)
          Exception?in?thread?"Thread-43"?java.lang.NumberFormatException:?multiple?points
          ?at?sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
          ?at?sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
          ?at?java.lang.Double.parseDouble(Double.java:538)
          ?at?java.text.DigitList.getDouble(DigitList.java:169)
          ?at?java.text.DecimalFormat.parse(DecimalFormat.java:2089)
          ?at?java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
          ?at?java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
          ?at?java.text.DateFormat.parse(DateFormat.java:364)
          ?at??.............

          其實解決這個問題很簡單,讓每個線程new一個自己的SimpleDateFormat,但是如果100個線程都要new100個SimpleDateFormat嗎?

          當(dāng)然我們不能這么做,我們可以借助線程池加上ThreadLocal來解決這個問題:

                
                public?class?SimpleDateFormatTest?{

          ????private?static?ThreadLocal<SimpleDateFormat>?local?=?new?ThreadLocal<SimpleDateFormat>()?{
          ????????@Override
          ???????//初始化線程本地變量
          ????????protected?SimpleDateFormat?initialValue()?{
          ????????????return?new?SimpleDateFormat("yyyy-MM-dd");
          ????????}
          ????};

          ????public?static?void?main(String[]?args)?{
          ????????ExecutorService?es?=?Executors.newCachedThreadPool();
          ????????for?(int?i?=?0;?i?<?500;?i++)?{
          ????????????es.execute(()?->?{
          ???????????????//調(diào)用字符串轉(zhuǎn)成日期方法
          ????????????????dateFormat();
          ????????????});
          ????????}
          ????????es.shutdown();
          ????}
          ????/**
          ?????*?字符串轉(zhuǎn)成日期類型
          ?????*/
          ????public?static?void?dateFormat()?{
          ????????try?{
          ???????????//ThreadLocal中的get()方法
          ????????????local.get().parse("2021-5-27");
          ????????}?catch?(ParseException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????}
          }

          這樣就優(yōu)雅的解決了線程安全問題;

          解決過度傳參問題;例如一個方法中要調(diào)用好多個方法,每個方法都需要傳遞參數(shù);例如下面示例:

                
                void?work(User?user)?{
          ????getInfo(user);
          ????checkInfo(user);
          ????setSomeThing(user);
          ????log(user);
          }

          用了ThreadLocal之后:

                
                public?class?ThreadLocalStu?{

          ????private?static?ThreadLocal<User>?userThreadLocal?=?new?ThreadLocal<>();

          ????void?work(User?user)?{
          ????????try?{
          ????????????userThreadLocal.set(user);
          ????????????getInfo();
          ????????????checkInfo();
          ????????????someThing();
          ????????}?finally?{
          ????????????userThreadLocal.remove();
          ????????}
          ????}

          ????void?setInfo()?{
          ????????User?u?=?userThreadLocal.get();
          ????????//.....
          ????}

          ????void?checkInfo()?{
          ????????User?u?=?userThreadLocal.get();
          ????????//....
          ????}

          ????void?someThing()?{
          ????????User?u?=?userThreadLocal.get();
          ????????//....
          ????}
          }

          每個線程內(nèi)需要保存全局變量(比如在登錄成功后將用戶信息存到ThreadLocal里,然后當(dāng)前線程操作的業(yè)務(wù)邏輯直接get取就完事了,有效的避免的參數(shù)來回傳遞的麻煩之處),一定層級上減少代碼耦合度。

          • 比如存儲 交易id等信息。每個線程私有。
          • 比如aop里記錄日志需要before記錄請求id,end拿出請求id,這也可以。
          • 比如jdbc連接池(很典型的一個ThreadLocal用法)
          • ....等等....

          原理分析

          上面我們基本上知道了ThreadLocal的使用方式以及應(yīng)用場景,當(dāng)然應(yīng)用場景不止這些這只是工作中常用到的場景;下面我們對它的原理進(jìn)行分析;

          我們先看一下它的set()方法;

                
                public?void?set(T?value)?{
          ????Thread?t?=?Thread.currentThread();
          ????ThreadLocalMap?map?=?getMap(t);
          ????if?(map?!=?null)
          ????????map.set(this,?value);
          ????else
          ????????createMap(t,?value);
          }

          是不是特別簡單,首先獲取當(dāng)前線程,用當(dāng)前線程作為key,去獲取ThreadLocalMap,然后判斷map是否為空,不為空就將當(dāng)前線程作為key,傳入的value作為map的value值;如果為空就創(chuàng)建一個ThreadLocalMap,然后將key和value方進(jìn)去;從這里可以看出value值是存放到ThreadLocalMap中;

          然后我們看看ThreadLocalMap是怎么來的?先看下getMap()方法:

                
                //在Thread類中維護(hù)了threadLocals變量,注意是Thread類
          ThreadLocal.ThreadLocalMap?threadLocals?=?null;?

          //在ThreadLocal類中的getMap()方法
          ThreadLocalMap?getMap(Thread?t)?{
          ????????return?t.threadLocals;
          ????}

          這就能解釋每個線程中都有一個ThreadLocalMap,因為ThreadLocalMap的引用在Thread中維護(hù);這就確保了線程間的隔離;

          我們繼續(xù)回到set()方法,看到當(dāng)map等于空的時候createMap(t, value);

                
                void?createMap(Thread?t,?T?firstValue)?{
          ??????t.threadLocals?=?new?ThreadLocalMap(this,?firstValue);
          ??}

          這里就是new了一個ThreadLocalMap然后賦值給threadLocals成員變量;ThreadLocalMap構(gòu)造方法:

                
                ThreadLocalMap(ThreadLocal<?>?firstKey,?Object?firstValue)?{
          ??//初始化一個Entry???
          ??table?=?new?Entry[INITIAL_CAPACITY];
          ???//計算key應(yīng)該存放的位置
          ???int?i?=?firstKey.threadLocalHashCode?&?(INITIAL_CAPACITY?-?1);
          ???//將Entry放到指定位置
          ???table[i]?=?new?Entry(firstKey,?firstValue);
          ???size?=?1;
          ???//設(shè)置數(shù)組的大小?16*2/3=10,類似HashMap中的0.75*16=12
          ???setThreshold(INITIAL_CAPACITY);
          ?}

          這里寫有個大概的印象,后面對ThreadLocalMap內(nèi)部結(jié)構(gòu)還會進(jìn)行詳細(xì)的講解;

          下面我們再去看一下get()方法:

                
                public?T?get()?{
          ????Thread?t?=?Thread.currentThread();
          ???//用當(dāng)前線程作為key去獲取ThreadLocalMap
          ????ThreadLocalMap?map?=?getMap(t);
          ????if?(map?!=?null)?{
          ???????//map不為空,然后獲取map中的Entry
          ????????ThreadLocalMap.Entry?e?=?map.getEntry(this);
          ????????if?(e?!=?null)?{
          ????????????@SuppressWarnings("unchecked")
          ???????????//如果Entry不為空就獲取對應(yīng)的value值
          ????????????T?result?=?(T)e.value;
          ????????????return?result;
          ????????}
          ????}
          ???//如果map為空或者entry為空的話通過該方法初始化,并返回該方法的value
          ????return?setInitialValue();
          }

          get()方法和set()都比較容易理解,如果map等于空的時候或者entry等于空的時候我們看看setInitialValue()方法做了什么事:

                
                ????private?T?setInitialValue()?{
          ??????//初始化變量值?由子類去實現(xiàn)并初始化變量
          ????????T?value?=?initialValue();
          ????????Thread?t?=?Thread.currentThread();
          ???????//這里再次getMap();
          ????????ThreadLocalMap?map?=?getMap(t);
          ????????if?(map?!=?null)
          ????????????map.set(this,?value);
          ????????else
          ??????//和set()方法中的
          ????????????createMap(t,?value);
          ????????return?value;
          ????}

          下面我們再去看一下ThreadLocal中的initialValue()方法:

                
                protected?T?initialValue()?{
          ?????return?null;
          ?}

          設(shè)置初始值,由子類去實現(xiàn);就例如我們上面的例子,重寫ThreadLocal類中的initialValue()方法:

                
                ????private?static?ThreadLocal<SimpleDateFormat>?local?=?new?ThreadLocal<SimpleDateFormat>()?{
          ????????@Override
          ???????//初始化線程本地變量
          ????????protected?SimpleDateFormat?initialValue()?{
          ????????????return?new?SimpleDateFormat("yyyy-MM-dd");
          ????????}
          ????};

          createMap()方法和上面set()方法中createMap()方法同一個,就不過多的敘述了;剩下還有一個removve()方法

                
                ???public?void?remove()?{
          ?????????ThreadLocalMap?m?=?getMap(Thread.currentThread());
          ?????????if?(m?!=?null)
          ???????????//2.?從map中刪除以當(dāng)前threadLocal實例為key的鍵值對
          ?????????????m.remove(this);
          ?????}

          源碼的講解就到這里,也都比較好理解,下面我們看看ThreadLocalMap的底層結(jié)構(gòu)

          ThreadLocalMap的底層結(jié)構(gòu)

          上面我們已經(jīng)了解了ThreadLocal的使用場景以及它比較重要的幾個方法;下面我們再去它的內(nèi)部結(jié)構(gòu);經(jīng)過上的源碼分析我們可以看到數(shù)據(jù)其實都是存放到了ThreadLocal中的內(nèi)部類ThreadLocalMap中;而ThreadLocalMap中又維護(hù)了一個Entry對象,也就說數(shù)據(jù)最終是存放到Entry對象中的;

                
                static?class?ThreadLocalMap?{

          ????????static?class?Entry?extends?WeakReference<ThreadLocal<?>>?{
          ????????????/**?The?value?associated?with?this?ThreadLocal.?*/
          ????????????Object?value;

          ????????????Entry(ThreadLocal<?>?k,?Object?v)?{
          ????????????????super(k);
          ????????????????value?=?v;
          ????????????}
          ?
          ????????}
          ??????????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);
          ????????}
          ??//?....................
          }

          Entry的構(gòu)造方法是以當(dāng)前線程為key,變量值Object為value進(jìn)行存儲的;在上面的源碼中ThreadLocalMap的構(gòu)造方法中也涉及到了Entry;看到Entry是一個數(shù)組;初始化長度為INITIAL_CAPACITY = 16;因為 Entry 繼承了 WeakReference,在 Entry 的構(gòu)造方法中,調(diào)用了 super(k)方法就會將 threadLocal 實例包裝成一個 WeakReferenece。這也是ThreadLocal會產(chǎn)生內(nèi)存泄露的原因;

          內(nèi)存泄露產(chǎn)生的原因

          338fa3aec0d5b24bc9332f6b1816371c.webp

          圖片

          如圖所示存在一條引用鏈:Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value,經(jīng)過上面的講解我們知道ThreadLocal作為Key,但是被設(shè)置成了弱引用,弱引用在JVM垃圾回收時是優(yōu)先回收的,就是說無論內(nèi)存是否足夠弱引用對象都會被回收;弱引用的生命周期比較短;當(dāng)發(fā)生一次GC的時候就會變成如下:

          31359b7681c5614ee73c804b8dad9d6d.webp

          圖片

          TreadLocalMap中出現(xiàn)了Key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果線程遲遲不結(jié)束(也就是說這條引用鏈無意義的一直存在)就會造成value永遠(yuǎn)無法回收造成內(nèi)存泄露;如果當(dāng)前線程運(yùn)行結(jié)束Thread,ThreadLocalMap,Entry之間沒有了引用鏈,在垃圾回收的時候就會被回收;但是在開發(fā)中我們都是使用線程池的方式,線程池的復(fù)用不會主動結(jié)束;所以還是會存在內(nèi)存泄露問題;

          解決方法也很簡單,就是在使用完之后主動調(diào)用remove()方法釋放掉;

          解決Hash沖突

          記得在大學(xué)學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)的時候?qū)W習(xí)了很多種解決hash沖突的方法;例如:

          線性探測法(開放地址法的一種): 計算出的散列地址如果已被占用,則按順序找下一個空位。如果找到末尾還沒有找到空位置就從頭重新開始找;

          159ca27cad2aa1404ed03e8c3e1e8f78.webp

          圖片

          二次探測法(開放地址法的一種)

          4c881c7faad611fb300814c8e4c829fd.webp

          圖片

          鏈地址法:鏈地址是對每一個同義詞都建一個單鏈表來解決沖突,HashMap采用的是這種方法;

          f88308a89bea0a3214a9ff0fed76b2c5.webp

          圖片

          多重Hash法: 在key沖突的情況下多重hash,直到不沖突為止,這種方式不易產(chǎn)生堆積但是計算量太大;

          公共溢出區(qū)法: 這種方式需要兩個表,一個存基礎(chǔ)數(shù)據(jù),另一個存放沖突數(shù)據(jù)稱為溢出表;

          上面的圖片都是在網(wǎng)上找到的一些資料,和大學(xué)時學(xué)習(xí)時的差不多我就直接拿來用了;也當(dāng)自己復(fù)習(xí)了一遍;

          介紹了那么多解決Hash沖突的方法,那ThreadLocalMap使用的哪一種方法呢?我們可以看一下源碼:

                
                ?private?void?set(ThreadLocal<?>?key,?Object?value)?{
          ????????????Entry[]?tab?=?table;
          ????????????int?len?=?tab.length;
          ???????//根據(jù)HashCode?&?數(shù)組長度?計算出數(shù)組該存放的位置
          ????????????int?i?=?key.threadLocalHashCode?&?(len-1);
          ?????//遍歷Entry數(shù)組中的元素
          ????????????for?(Entry?e?=?tab[i];
          ?????????????????e?!=?null;
          ?????????????????e?=?tab[i?=?nextIndex(i,?len)])?{
          ????????????????ThreadLocal<?>?k?=?e.get();
          ??????//如果這個Entry對象的key正好是即將設(shè)置的key,那么就刷新Entry中的value;
          ????????????????if?(k?==?key)?{
          ????????????????????e.value?=?value;
          ????????????????????return;
          ????????????????}
          ?????//?entry!=null,key==null時,說明threadLcoal這key已經(jīng)被GC了,這里就是上面說到
          ?????//會有內(nèi)存泄露的地方,當(dāng)然作者也知道這種情況的存在,所以這里做了一個判斷進(jìn)行解決臟的
          ?????//entry(數(shù)組中不想存有過時的entry),但是也不能解決泄露問題,因為舊value還存在沒有消失
          ????????????????if?(k?==?null)?{
          ??????????????????//用當(dāng)前插入的值代替掉這個key為null的“臟”entry
          ????????????????????replaceStaleEntry(key,?value,?i);
          ????????????????????return;
          ????????????????}
          ????????????}
          ????//新建entry并插入table中i處
          ????????????tab[i]?=?new?Entry(key,?value);
          ????????????int?sz?=?++size;
          ????????????if?(!cleanSomeSlots(i,?sz)?&&?sz?>=?threshold)
          ????????????????rehash();
          ????????}

          從這里我們可以看出使用的是線性探測的方式來解決hash沖突!

          源碼中通過nextIndex(i, len)方法解決 hash 沖突的問題,該方法為((i + 1 < len) ? i + 1 : 0);,也就是不斷往后線性探測,直到找到一個空的位置,當(dāng)?shù)焦1砟┪驳臅r候還沒有找到空位置再從 0 開始找,成環(huán)形!

          使用ThreadLocal時對象存在哪里?

          在java中,棧內(nèi)存歸屬于單個線程,每個線程都會有一個棧內(nèi)存,其存儲的變量只能在其所屬線程中可見,即棧內(nèi)存可以理解成線程的私有變量,而堆內(nèi)存中的變量對所有線程可見,可以被所有線程訪問!

          那么ThreadLocal的實例以及它的值是不是存放在棧上呢?其實不是的,因為ThreadLocal的實例實際上也是被其創(chuàng)建的類持有,(更頂端應(yīng)該是被線程持有),而ThreadLocal的值其實也是被線程實例持有,它們都是位于堆上,只是通過一些技巧將可見性修改成了線程可見。

                
                  

          (完)

                  

          碼農(nóng)突圍資料鏈接

          1、臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載!
          2、計算機(jī)基礎(chǔ)知識總結(jié)與操作系統(tǒng) PDF 下載
          3、艾瑪,終于來了!《LeetCode Java版題解》.PDF
          4、Github 10K+,《LeetCode刷題C/C++版答案》出爐.PDF

          歡迎添加魚哥個人微信:smartfish2020,進(jìn)粉絲群或圍觀朋友圈

          瀏覽 49
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  e133日韩无码 | 午夜精品久久久久久不卡8050 | 日本反a大片 | 啪啪啪视频免费看 | 日韩一级黄色免费电影网站 |