掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流

ThreadLocal算是一種并發(fā)容器吧,因?yàn)樗膬?nèi)部是有ThreadLocalMap組成,ThreadLocal是為了解決多線程情況下變量不能被共享的問(wèn)題,也就是多線程共享變量的問(wèn)題。
ThreadLocal和Lock以及Synchronized的區(qū)別是:ThreadLocal是給每個(gè)線程分配一個(gè)變量(對(duì)象),各個(gè)線程都存有變量的副本,這樣每個(gè)線程都是使用自己(變量)對(duì)象實(shí)例,使線程與線程之間進(jìn)行隔離;而Lock和Synchronized的方式是使線程有順序的執(zhí)行。
舉一個(gè)簡(jiǎn)單的例子:目前有100個(gè)學(xué)生等待簽字,但是老師只有一個(gè)筆,那老師只能按順序的分給每個(gè)學(xué)生,等待A學(xué)生簽字完成然后將筆交給B學(xué)生,這就類似Lock,Synchronized的方式。而ThreadLocal是,老師直接拿出一百個(gè)筆給每個(gè)學(xué)生;再效率提高的同事也要付出一個(gè)內(nèi)存消耗;也就是以空間換時(shí)間的概念
Spring的事務(wù)隔離就是使用ThreadLocal和AOP來(lái)解決的;主要是TransactionSynchronizationManager這個(gè)類;
解決SimpleDateFormat線程不安全問(wèn)題;
當(dāng)我們使用SimpleDateFormat的parse()方法的時(shí)候,parse()方法會(huì)先調(diào)用Calendar.clear()方法,然后調(diào)用Calendar.add()方法,如果一個(gè)線程先調(diào)用了add()方法,然后另一個(gè)線程調(diào)用了clear()方法;這時(shí)候parse()方法就會(huì)出現(xiàn)解析錯(cuò)誤;如果不信我們可以來(lái)個(gè)例子:
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();
}
}
}這里我們只啟動(dòng)了50個(gè)線程問(wèn)題就會(huì)出現(xiàn),其實(shí)看巧不巧,有時(shí)候只有10個(gè)線程的情況就會(huì)出錯(cuò):
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 .............
其實(shí)解決這個(gè)問(wèn)題很簡(jiǎn)單,讓每個(gè)線程new一個(gè)自己的SimpleDateFormat,但是如果100個(gè)線程都要new100個(gè)SimpleDateFormat嗎?
當(dāng)然我們不能這么做,我們可以借助線程池加上ThreadLocal來(lái)解決這個(gè)問(wèn)題:
public class SimpleDateFormatTest {
private static ThreadLocal local = new ThreadLocal() {
@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)雅的解決了線程安全問(wèn)題;
解決過(guò)度傳參問(wèn)題;例如一個(gè)方法中要調(diào)用好多個(gè)方法,每個(gè)方法都需要傳遞參數(shù);例如下面示例:
void work(User user) {
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}用了ThreadLocal之后:
public class ThreadLocalStu {
private static ThreadLocal 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();
//....
}
} 每個(gè)線程內(nèi)需要保存全局變量(比如在登錄成功后將用戶信息存到ThreadLocal里,然后當(dāng)前線程操作的業(yè)務(wù)邏輯直接get取就完事了,有效的避免的參數(shù)來(lái)回傳遞的麻煩之處),一定層級(jí)上減少代碼耦合度。
上面我們基本上知道了ThreadLocal的使用方式以及應(yīng)用場(chǎng)景,當(dāng)然應(yīng)用場(chǎng)景不止這些這只是工作中常用到的場(chǎng)景;下面我們對(duì)它的原理進(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);
}是不是特別簡(jiǎn)單,首先獲取當(dāng)前線程,用當(dāng)前線程作為key,去獲取ThreadLocalMap,然后判斷map是否為空,不為空就將當(dāng)前線程作為key,傳入的value作為map的value值;如果為空就創(chuàng)建一個(gè)ThreadLocalMap,然后將key和value方進(jìn)去;從這里可以看出value值是存放到ThreadLocalMap中;
然后我們看看ThreadLocalMap是怎么來(lái)的?先看下getMap()方法:
//在Thread類中維護(hù)了threadLocals變量,注意是Thread類
ThreadLocal.ThreadLocalMap threadLocals = null;
//在ThreadLocal類中的getMap()方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
這就能解釋每個(gè)線程中都有一個(gè)ThreadLocalMap,因?yàn)門(mén)hreadLocalMap的引用在Thread中維護(hù);這就確保了線程間的隔離;
我們繼續(xù)回到set()方法,看到當(dāng)map等于空的時(shí)候createMap(t, value);
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}這里就是new了一個(gè)ThreadLocalMap然后賦值給threadLocals成員變量;ThreadLocalMap構(gòu)造方法:
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
//初始化一個(gè)Entry
table = new Entry[INITIAL_CAPACITY];
//計(jì)算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);
}這里寫(xiě)有個(gè)大概的印象,后面對(duì)ThreadLocalMap內(nèi)部結(jié)構(gòu)還會(huì)進(jìn)行詳細(xì)的講解;
下面我們?cè)偃タ匆幌耮et()方法:
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不為空就獲取對(duì)應(yīng)的value值
T result = (T)e.value;
return result;
}
}
//如果map為空或者entry為空的話通過(guò)該方法初始化,并返回該方法的value
return setInitialValue();
}get()方法和set()都比較容易理解,如果map等于空的時(shí)候或者entry等于空的時(shí)候我們看看setInitialValue()方法做了什么事:
private T setInitialValue() {
//初始化變量值 由子類去實(shí)現(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;
}下面我們?cè)偃タ匆幌耇hreadLocal中的initialValue()方法:
protected T initialValue() {
return null;
}設(shè)置初始值,由子類去實(shí)現(xiàn);就例如我們上面的例子,重寫(xiě)ThreadLocal類中的initialValue()方法:
private static ThreadLocallocal = new ThreadLocal () {
@Override
//初始化線程本地變量
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
createMap()方法和上面set()方法中createMap()方法同一個(gè),就不過(guò)多的敘述了;剩下還有一個(gè)removve()方法。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//2. 從map中刪除以當(dāng)前threadLocal實(shí)例為key的鍵值對(duì)
m.remove(this);
}源碼的講解就到這里,也都比較好理解,下面我們看看ThreadLocalMap的底層結(jié)構(gòu)。
上面我們已經(jīng)了解了ThreadLocal的使用場(chǎng)景以及它比較重要的幾個(gè)方法;下面我們?cè)偃ニ膬?nèi)部結(jié)構(gòu);經(jīng)過(guò)上的源碼分析我們可以看到數(shù)據(jù)其實(shí)都是存放到了ThreadLocal中的內(nèi)部類ThreadLocalMap中;而ThreadLocalMap中又維護(hù)了一個(gè)Entry對(duì)象,也就說(shuō)數(shù)據(jù)最終是存放到Entry對(duì)象中的;
static class ThreadLocalMap {
static class Entry extends WeakReference> {
/** 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)行存儲(chǔ)的;在上面的源碼中ThreadLocalMap的構(gòu)造方法中也涉及到了Entry;看到Entry是一個(gè)數(shù)組;初始化長(zhǎng)度為INITIAL_CAPACITY = 16;因?yàn)?Entry 繼承了 WeakReference,在 Entry 的構(gòu)造方法中,調(diào)用了 super(k)方法就會(huì)將 threadLocal 實(shí)例包裝成一個(gè) WeakReferenece。這也是ThreadLocal會(huì)產(chǎn)生內(nèi)存泄露的原因;
圖片
如圖所示存在一條引用鏈:Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value,經(jīng)過(guò)上面的講解我們知道ThreadLocal作為Key,但是被設(shè)置成了弱引用,弱引用在JVM垃圾回收時(shí)是優(yōu)先回收的,就是說(shuō)無(wú)論內(nèi)存是否足夠弱引用對(duì)象都會(huì)被回收;弱引用的生命周期比較短;當(dāng)發(fā)生一次GC的時(shí)候就會(huì)變成如下:
圖片
TreadLocalMap中出現(xiàn)了Key為null的Entry,就沒(méi)有辦法訪問(wèn)這些key為null的Entry的value,如果線程遲遲不結(jié)束(也就是說(shuō)這條引用鏈無(wú)意義的一直存在)就會(huì)造成value永遠(yuǎn)無(wú)法回收造成內(nèi)存泄露;如果當(dāng)前線程運(yùn)行結(jié)束Thread,ThreadLocalMap,Entry之間沒(méi)有了引用鏈,在垃圾回收的時(shí)候就會(huì)被回收;但是在開(kāi)發(fā)中我們都是使用線程池的方式,線程池的復(fù)用不會(huì)主動(dòng)結(jié)束;所以還是會(huì)存在內(nèi)存泄露問(wèn)題;
解決方法也很簡(jiǎn)單,就是在使用完之后主動(dòng)調(diào)用remove()方法釋放掉;
記得在大學(xué)學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)的時(shí)候?qū)W習(xí)了很多種解決hash沖突的方法;例如:
線性探測(cè)法(開(kāi)放地址法的一種): 計(jì)算出的散列地址如果已被占用,則按順序找下一個(gè)空位。如果找到末尾還沒(méi)有找到空位置就從頭重新開(kāi)始找;
圖片
二次探測(cè)法(開(kāi)放地址法的一種)
圖片
鏈地址法:鏈地址是對(duì)每一個(gè)同義詞都建一個(gè)單鏈表來(lái)解決沖突,HashMap采用的是這種方法;
圖片
多重Hash法: 在key沖突的情況下多重hash,直到不沖突為止,這種方式不易產(chǎn)生堆積但是計(jì)算量太大;
公共溢出區(qū)法: 這種方式需要兩個(gè)表,一個(gè)存基礎(chǔ)數(shù)據(jù),另一個(gè)存放沖突數(shù)據(jù)稱為溢出表;
上面的圖片都是在網(wǎng)上找到的一些資料,和大學(xué)時(shí)學(xué)習(xí)時(shí)的差不多我就直接拿來(lái)用了;也當(dāng)自己復(fù)習(xí)了一遍;
介紹了那么多解決Hash沖突的方法,那ThreadLocalMap使用的哪一種方法呢?我們可以看一下源碼:
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根據(jù)HashCode & 數(shù)組長(zhǎng)度 計(jì)算出數(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();
//如果這個(gè)Entry對(duì)象的key正好是即將設(shè)置的key,那么就刷新Entry中的value;
if (k == key) {
e.value = value;
return;
}
// entry!=null,key==null時(shí),說(shuō)明threadLcoal這key已經(jīng)被GC了,這里就是上面說(shuō)到
//會(huì)有內(nèi)存泄露的地方,當(dāng)然作者也知道這種情況的存在,所以這里做了一個(gè)判斷進(jìn)行解決臟的
//entry(數(shù)組中不想存有過(guò)時(shí)的entry),但是也不能解決泄露問(wèn)題,因?yàn)榕fvalue還存在沒(méi)有消失
if (k == null) {
//用當(dāng)前插入的值代替掉這個(gè)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();
}從這里我們可以看出使用的是線性探測(cè)的方式來(lái)解決hash沖突!
源碼中通過(guò)nextIndex(i, len)方法解決 hash 沖突的問(wèn)題,該方法為((i + 1 < len) ? i + 1 : 0);,也就是不斷往后線性探測(cè),直到找到一個(gè)空的位置,當(dāng)?shù)焦1砟┪驳臅r(shí)候還沒(méi)有找到空位置再?gòu)?0 開(kāi)始找,成環(huán)形!
使用ThreadLocal時(shí)對(duì)象存在哪里?
在java中,棧內(nèi)存歸屬于單個(gè)線程,每個(gè)線程都會(huì)有一個(gè)棧內(nèi)存,其存儲(chǔ)的變量只能在其所屬線程中可見(jiàn),即棧內(nèi)存可以理解成線程的私有變量,而堆內(nèi)存中的變量對(duì)所有線程可見(jiàn),可以被所有線程訪問(wèn)!
那么ThreadLocal的實(shí)例以及它的值是不是存放在棧上呢?其實(shí)不是的,因?yàn)門(mén)hreadLocal的實(shí)例實(shí)際上也是被其創(chuàng)建的類持有,(更頂端應(yīng)該是被線程持有),而ThreadLocal的值其實(shí)也是被線程實(shí)例持有,它們都是位于堆上,只是通過(guò)一些技巧將可見(jiàn)性修改成了線程可見(jiàn)。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流