ThreadLocal 你真的會用嗎?
點擊關(guān)注公眾號,Java干貨 及時送達(dá) ??
來源: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是為了解決多線程情況下變量不能被共享的問題,也就是多線程共享變量的問題。
ThreadLocal和Lock以及Synchronized的區(qū)別是:ThreadLocal是給每個線程分配一個變量(對象),各個線程都存有變量的副本,這樣每個線程都是使用自己(變量)對象實例,使線程與線程之間進(jìn)行隔離;而Lock和Synchronized的方式是使線程有順序的執(zhí)行。
舉一個簡單的例子:目前有100個學(xué)生等待簽字,但是老師只有一個筆,那老師只能按順序的分給每個學(xué)生,等待A學(xué)生簽字完成然后將筆交給B學(xué)生,這就類似Lock,Synchronized的方式。而ThreadLocal是,老師直接拿出一百個筆給每個學(xué)生;再效率提高的同事也要付出一個內(nèi)存消耗;也就是以空間換時間的概念
使用場景
Spring的事務(wù)隔離就是使用ThreadLocal和AOP來解決的;主要是TransactionSynchronizationManager這個類;
解決SimpleDateFormat線程不安全問題;
當(dāng)我們使用SimpleDateFormat的parse()方法的時候,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)生的原因

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

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

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

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

多重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)粉絲群或圍觀朋友圈
