服了,一個(gè)ThreadLocal被問出了花
一、故事
地鐵上,小帥無力地倚靠著桿子,腦子里盡是剛才面試官的奪命連環(huán)問,“用過TheadLocal么?ThreadLocal是如何解決共享變量訪問的安全性的呢?你覺得啥場(chǎng)景下會(huì)用到TheadLocal? 我們?cè)谌粘S肨hreadLocal的時(shí)候需要注意什么?ThreadLocal在高并發(fā)場(chǎng)景下會(huì)造成內(nèi)存泄漏嗎?為什么?如何避免?......”
這些問題,如同陰影一般,在小帥的腦海里揮之不去。
是的,他萬萬沒想到,自詡“多線程小能手”的他栽在了ThreadLocal上。
這是小帥苦投了半個(gè)月簡(jiǎn)歷之后才拿到的面試機(jī)會(huì),然而又喪失了。當(dāng)下行情實(shí)在是卷到了極點(diǎn)。
都兩個(gè)月了,面試機(jī)會(huì)少,居然還每次都被問翻,這樣下去真要回老家另謀出路了,小帥內(nèi)心五味成雜......
小伙伴們,試問一下,如果是你,面對(duì)上述的問題,你能否對(duì)答如流呢?
二、概要
既然被問到了,那么作為事后諸葛的老貓就和大家一起來接面試官的招吧。
我們將從以下點(diǎn)來全面剖析一下ThreadLocal。
概覽
三、基本篇
1.什么是ThreadLocal?
ThreadLocal英文翻譯過來就是:線程本地量,它其實(shí)是一種線程的隔離機(jī)制,保障了多線程環(huán)境下對(duì)于共享變量訪問的安全性。
看到上面的定義之后,那么問題就來了,ThreadLocal是如何解決共享變量訪問的安全性的呢?
其實(shí)ThreadLocal為變量在每個(gè)線程中都創(chuàng)建了一個(gè)副本,那么每個(gè)線程可以訪問自己內(nèi)部的副本變量。由于副本都?xì)w屬于各自的線程,所以就不存在多線程共享的問題了。
便于理解,我們看一下下圖。
結(jié)構(gòu)圖
至于上述圖中提及的threadLocals(ThreadLocalMap),我們后文看源代碼的時(shí)候再繼續(xù)來看。大家心中暫時(shí)有個(gè)概念。
既然都是保證線程訪問的安全性,那么和Synchronized區(qū)別是什么呢?
在上面聊到共享變量訪問安全性的問題上,其實(shí)大家還會(huì)很容易想起另外一個(gè)關(guān)鍵字Synchronized。聊聊區(qū)別吧,整理了一張圖,看起來可能會(huì)更加直觀一些,如下。
對(duì)比
通過上圖,我們發(fā)現(xiàn)ThreadLocal其實(shí)是一種線程隔離機(jī)制。Synchronized則是一種基于Happens-Before規(guī)則里的監(jiān)視器鎖規(guī)則從而保證同一個(gè)時(shí)刻只有一個(gè)線程能夠?qū)蚕碜兞窟M(jìn)行更新。
Synchronized加鎖會(huì)帶來性能上的下降。ThreadLocal采用了空間換時(shí)間的設(shè)計(jì)思想,也就是說每個(gè)線程里面都有一個(gè)專門的容器來存儲(chǔ)共享變量的副本信息,然后每個(gè)線程只對(duì)自己的變量副本做相對(duì)應(yīng)的更新操作,這樣避免了多線程鎖競(jìng)爭(zhēng)的開銷。
2.ThreadLocal的使用
上面說了這么多,咱們來使用一下。就拿SimpleDateFormat來做個(gè)例子。當(dāng)然也會(huì)有一道這樣的面試題,SimpleDateFormat是否是線程安全的?在阿里Java開發(fā)規(guī)約中,有強(qiáng)制性地提到SimpleDateFormat 是線程不安全的類。其實(shí)主要的原因是由于多線程操作SimpleDateFormat中的Calendar對(duì)象引用,然后出現(xiàn)臟讀導(dǎo)致的。
踩坑代碼:
/**
* @author 公眾號(hào):程序員老貓
* @date 2024/2/1 22:58
*/
public class DateFormatTest {
private static final SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parse(String dateString) {
Date date = null;
try {
date = simpleDateFormat.parse(dateString);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
executorService.execute(()->{
System.out.println(parse("2024-02-01 23:34:30"));
});
}
executorService.shutdown();
}
}
上述咱們通過線程池的方式針對(duì)SimpleDateFormat進(jìn)行了測(cè)試。其輸出結(jié)果如下。
我們可以看到剛開始好好的,后面就異常了。
我們通過ThreadLocal的方式將其優(yōu)化一下。代碼如下:
/**
* @author 公眾號(hào):程序員老貓
* @date 2024/2/1 22:58
*/
public class DateFormatTest {
private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String dateString) {
Date date = null;
try {
date = dateFormatThreadLocal.get().parse(dateString);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
executorService.execute(()->{
System.out.println(parse("2024-02-01 23:34:30"));
});
}
executorService.shutdown();
}
}
運(yùn)行了一下,完全正常了。
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
3.TheadLocal使用場(chǎng)景
那么我們什么時(shí)候會(huì)用到ThreadLocal呢?
上面針對(duì)SimpleDateFormat的封裝也算是一個(gè)吧。
- 用來替代參數(shù)鏈傳遞:在編寫API接口時(shí),可以將需要傳遞的參數(shù)放入ThreadLocal中,從而不需要在每個(gè)調(diào)用的方法上都顯式地傳遞這些參數(shù)。這種方法雖然不如將參數(shù)封裝為對(duì)象傳遞來得常見,但在某些情況下可以簡(jiǎn)化代碼結(jié)構(gòu)。
- 數(shù)據(jù)庫連接和會(huì)話管理:在某些應(yīng)用中,如Web應(yīng)用程序,ThreadLocal可以用來保持對(duì)數(shù)據(jù)庫連接或會(huì)話的管理,以簡(jiǎn)化并發(fā)控制并提高性能。例如,可以使用ThreadLocal來維護(hù)一個(gè)連接池,使得每個(gè)請(qǐng)求都能共享相同的連接,而不是每次都需要重新建立連接。
- 全局存儲(chǔ)信息:例如在前后端分離的應(yīng)用中,ThreadLocal可以用來在服務(wù)端維護(hù)用戶的上下文信息或者一些配置信息,而不需要通過HTTP請(qǐng)求攜帶大量的用戶信息。這樣做可以在不改變?cè)屑軜?gòu)的情況下,提供更好的用戶體驗(yàn)。
如果大家還能想到其他使用的場(chǎng)景也歡迎留言。
四、升華篇
1.ThreadLocal原理
上述其實(shí)咱們聊得相對(duì)而言還是比較淺的。那么接下來,咱們豐富一下之前提到的結(jié)構(gòu)圖,從源代碼側(cè)深度剖一下ThreadLocal吧。
深度結(jié)構(gòu)圖
對(duì)應(yīng)上述圖中,解釋一下。
- 圖中有兩個(gè)線程Thread1以及Thread2。
- Thread類中有一個(gè)叫做threadLocals的成員變量,它是ThreadLocal.ThreadLocalMap類型的。
- ThreadLocalMap內(nèi)部維護(hù)了Entry數(shù)組,每個(gè)Entry代表一個(gè)完整的對(duì)象,key是ThreadLocal本身,value是ThreadLocal的泛型對(duì)象值。
對(duì)應(yīng)的我們看一下Thread的源代碼,如下:
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
在源碼中threadLocals的初始值為Null。
抽絲剝繭,咱們繼續(xù)看一下ThreadLocalMap在調(diào)用構(gòu)造函數(shù)進(jìn)行初始化的源代碼:
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16; //初始化容量
private Entry[] table; //ThreadLocalMap數(shù)據(jù)真正存儲(chǔ)在table中
private int size = 0; //ThreadLocalMap條數(shù)
private int threshold; // 默認(rèn)為0,達(dá)到這個(gè)大小,則擴(kuò)容
//類Entry的實(shí)現(xiàn)
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//構(gòu)造函數(shù)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY]; //初始化table數(shù)組,INITIAL_CAPACITY默認(rèn)值為16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //key和16取得哈希值
table[i] = new Entry(firstKey, firstValue);//創(chuàng)建節(jié)點(diǎn),設(shè)置key-value
size = 1;
setThreshold(INITIAL_CAPACITY); //設(shè)置擴(kuò)容閾值
}
}
在源碼中涉及比較核心的還有set,get以及remove方法。我們依次來看一下:
set方法如下:
public void set(T value) {
Thread t = Thread.currentThread(); //獲取當(dāng)前線程t
ThreadLocalMap map = getMap(t); //根據(jù)當(dāng)前線程獲取到ThreadLocalMap
if (map != null) //如果獲取的ThreadLocalMap對(duì)象不為空
map.set(this, value); //K,V設(shè)置到ThreadLocalMap中
else
createMap(t, value); //創(chuàng)建一個(gè)新的ThreadLocalMap
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //返回Thread對(duì)象的ThreadLocalMap屬性
}
void createMap(Thread t, T firstValue) { //調(diào)用ThreadLocalMap的構(gòu)造函數(shù)
t.threadLocals = new ThreadLocalMap(this, firstValue); //this表示當(dāng)前類ThreadLocal
}
get方法如下:
public T get() {
//1、獲取當(dāng)前線程
Thread t = Thread.currentThread();
//2、獲取當(dāng)前線程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果map數(shù)據(jù)不為空,
if (map != null) {
//3.1、獲取threalLocalMap中存儲(chǔ)的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是數(shù)據(jù)為null,則初始化,初始化的結(jié)果,TheralLocalMap中存放key值為threadLocal,值為null
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
remove方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
那么為什么需要remove方法呢?其實(shí)這里會(huì)涉及到內(nèi)存泄漏的問題了。后面咱們細(xì)看。
對(duì)照著上述的結(jié)構(gòu)圖以及源碼,如果面試官問ThreadLocal原理的時(shí)候,相信大家應(yīng)該可以說出個(gè)所以然來。
- Thread線程類有一個(gè)類型為ThreadLocal.ThreadLocalMap的變量threadLocals,即每個(gè)線程都有一個(gè)屬于自己的ThreadLocalMap。
- ThreadLocalMap方法內(nèi)部維護(hù)著Entry數(shù)組,其中key是ThreadLocal本身,而value則為其泛型值。
- 并發(fā)場(chǎng)景下,每個(gè)線程都會(huì)存儲(chǔ)當(dāng)前變量副本到自己的ThreadLocalMap中,后續(xù)這個(gè)線程對(duì)于共享變量的操作,都是從TheadLocalMap里進(jìn)行變更,不會(huì)影響全局共享變量的值。
2.高并發(fā)場(chǎng)景下ThreadLocal會(huì)造成內(nèi)存泄漏嗎?什么原因?qū)е拢咳绾伪苊猓?/h4>
(1) 造成內(nèi)存泄漏的原因
這個(gè)問題其實(shí)還是得從ThreadLocal底層源碼的實(shí)現(xiàn)去看。高并發(fā)場(chǎng)景下,如果對(duì)ThreadLocal處理得當(dāng)?shù)脑捚鋵?shí)就不會(huì)造成內(nèi)存泄漏。我們看下面這樣一組源代碼片段:
static class ThreadLocalMap {
...
//類Entry的實(shí)現(xiàn)
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
上文中其實(shí)我們已經(jīng)知道Entry中以key和value的形式存儲(chǔ),key是ThreadLocal本身,上面代碼中我們看到entry進(jìn)行key設(shè)置的時(shí)候用的是super(k)。那就意味著調(diào)用的父類的方法去設(shè)置了key,我們?cè)倏匆幌赂割愂鞘裁?,父類其?shí)是WeakReference。關(guān)于WeakReference底層的實(shí)現(xiàn),大家有興趣可以展開去看看源代碼,老貓?jiān)谶@里直接說結(jié)果。
WeakReference 如字面意思,弱引用,當(dāng)一個(gè)對(duì)象僅僅被weak reference(弱引用)指向, 而沒有任何其他strong reference(強(qiáng)引用)指向的時(shí)候, 如果這時(shí)GC運(yùn)行, 那么這個(gè)對(duì)象就會(huì)被回收,不論當(dāng)前的內(nèi)存空間是否足夠,這個(gè)對(duì)象都會(huì)被回收。
關(guān)于這些引用的強(qiáng)弱,稍微聊一下,這里其實(shí)涉及到j(luò)vm的回收機(jī)制。在JDK1.2之后,java對(duì)引用的概念其實(shí)做了擴(kuò)充的,分為強(qiáng)引用,軟引用,弱引用,虛引用。
- 強(qiáng)引用:其實(shí)就是咱們一般用“=”的賦值行為,如 Student s = new Student(),只要強(qiáng)引用還在,對(duì)象就不會(huì)被回收。
- 軟引用:不是必須存活的對(duì)象,jvm在內(nèi)存不夠的情況下即將內(nèi)存溢出前會(huì)對(duì)其進(jìn)行回收。例如緩存。
- 弱引用:非必須存活的對(duì)象,引用關(guān)系比軟引用還弱,無論內(nèi)存夠還是不夠,下次的GC一定會(huì)被回收。
- 虛引用:別名幽靈引用或者幻影引用。等同于沒有引用,唯一的目的是對(duì)象被回收的時(shí)候會(huì)受到系統(tǒng)通知。
明白這些概念之后,咱們?cè)倏纯瓷厦娴脑创a,我們就會(huì)發(fā)現(xiàn),原來Key其實(shí)是弱引用,而里面的value因?yàn)槭侵苯淤x值行為所以是強(qiáng)引用。
如下圖:
jvm存儲(chǔ)
圖中我們可以看到由于threadLocal對(duì)象是弱引用,如果外部沒有強(qiáng)引用指向的話,它就會(huì)被GC回收,那么這個(gè)時(shí)候?qū)е翬ntry的key就為NULL,如果此時(shí)value外部也沒有強(qiáng)引用指向的話,那么這個(gè)value就永遠(yuǎn)無法訪問了,按道理也該被回收。但是由于entry還在強(qiáng)引用value(看源代碼)。那么此時(shí)value就無法被回收,此時(shí)內(nèi)存泄漏就出現(xiàn)了。本質(zhì)原因是因?yàn)関alue成為了一個(gè)永遠(yuǎn)無法被訪問也無法被回收的對(duì)象。
那肯定有小伙伴會(huì)有疑問了,線程本身生命周期不是很短么,如果短時(shí)間內(nèi)被銷毀,就不會(huì)內(nèi)存泄漏了,因?yàn)橹灰€程銷毀,那么value也會(huì)被回收。這話是沒錯(cuò)。但是咱們的線程是計(jì)算機(jī)珍貴資源,為了避免重復(fù)創(chuàng)建線程帶來開銷,系統(tǒng)中我們往往會(huì)使用線程池(線程池傳送門),如果使用線程池的話,那么線程的生命周期就被拉長(zhǎng)了,那么就可想而知了。
(2) 如何避免
解法如下:
- 每次使用完畢之后記得調(diào)用一下remove()方法清除數(shù)據(jù)。
- ThreadLocal變量盡量定義成static final類型,避免頻繁創(chuàng)建ThreadLocal實(shí)例。這樣可以保證程序中一直存在ThreadLocal強(qiáng)引用,也能保證任何時(shí)候都能通過ThreadLocal的弱引用訪問Entry的value值,從而進(jìn)行清除。
不過話說回來,其實(shí)ThreadLocal內(nèi)部也做了優(yōu)化的。在set()的時(shí)候也會(huì)采樣清理,擴(kuò)容的時(shí)候也會(huì)檢查(這里希望大家自己深入看一下源代碼),在get()的時(shí)候,如果沒有直接命中或者向后環(huán)形查找的時(shí)候也會(huì)進(jìn)行清理。但是為了系統(tǒng)的穩(wěn)健萬無一失,所以大家盡量還是將上面的兩個(gè)注意點(diǎn)在寫代碼的時(shí)候注意下。
總結(jié)
面試的時(shí)候大家總會(huì)去背一些八股文,但是這種也只是臨時(shí)應(yīng)付面試官而已,真正的懂其中的原理才是硬道理。無論咋問,萬變不離核心原理。當(dāng)然這些核心原理在我們的日常編碼中也會(huì)給我們帶來很大的幫助,用法很簡(jiǎn)單,翻車了如何處理,那還不是得知其所以然么,伙伴們,你們覺得呢?