地鐵上,小帥無力地倚靠著桿子,腦子里盡是剛才面試官的奪命連環(huán)問,“用過TheadLocal么?ThreadLocal是如何解決共享變量訪問的安全性的呢?你覺得啥場景下會用到TheadLocal? 我們在日常用ThreadLocal的時候需要注意什么?ThreadLocal在高并發(fā)場景下會造成內(nèi)存泄漏嗎?為什么?如何避免?......”
這些問題,如同陰影一般,在小帥的腦海里揮之不去。
是的,他萬萬沒想到,自詡“多線程小能手”的他栽在了ThreadLocal上。
這是小帥苦投了半個月簡歷之后才拿到的面試機會,然而又喪失了。當下行情實在是卷到了極點。
都兩個月了,面試機會少,居然還每次都被問翻,這樣下去真要回老家另謀出路了,小帥內(nèi)心五味成雜......
小伙伴們,試問一下,如果是你,面對上述的問題,你能否對答如流呢?
既然被問到了,那么作為事后諸葛的老貓就和大家一起來接面試官的招吧。
我們將從以下點來全面剖析一下ThreadLocal。
概覽
ThreadLocal英文翻譯過來就是:線程本地量,它其實是一種線程的隔離機制,保障了多線程環(huán)境下對于共享變量訪問的安全性。
看到上面的定義之后,那么問題就來了,ThreadLocal是如何解決共享變量訪問的安全性的呢?
其實ThreadLocal為變量在每個線程中都創(chuàng)建了一個副本,那么每個線程可以訪問自己內(nèi)部的副本變量。由于副本都歸屬于各自的線程,所以就不存在多線程共享的問題了。
便于理解,我們看一下下圖。
結(jié)構(gòu)圖
至于上述圖中提及的threadLocals(ThreadLocalMap),我們后文看源代碼的時候再繼續(xù)來看。大家心中暫時有個概念。
既然都是保證線程訪問的安全性,那么和Synchronized區(qū)別是什么呢?
在上面聊到共享變量訪問安全性的問題上,其實大家還會很容易想起另外一個關鍵字Synchronized。聊聊區(qū)別吧,整理了一張圖,看起來可能會更加直觀一些,如下。
對比
通過上圖,我們發(fā)現(xiàn)ThreadLocal其實是一種線程隔離機制。Synchronized則是一種基于Happens-Before規(guī)則里的監(jiān)視器鎖規(guī)則從而保證同一個時刻只有一個線程能夠?qū)蚕碜兞窟M行更新。
Synchronized加鎖會帶來性能上的下降。ThreadLocal采用了空間換時間的設計思想,也就是說每個線程里面都有一個專門的容器來存儲共享變量的副本信息,然后每個線程只對自己的變量副本做相對應的更新操作,這樣避免了多線程鎖競爭的開銷。
上面說了這么多,咱們來使用一下。就拿SimpleDateFormat來做個例子。當然也會有一道這樣的面試題,SimpleDateFormat是否是線程安全的?在阿里Java開發(fā)規(guī)約中,有強制性地提到SimpleDateFormat 是線程不安全的類。其實主要的原因是由于多線程操作SimpleDateFormat中的Calendar對象引用,然后出現(xiàn)臟讀導致的。
踩坑代碼:
/** * @author 公眾號:程序員老貓 * @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(); }}
上述咱們通過線程池的方式針對SimpleDateFormat進行了測試。其輸出結(jié)果如下。
我們可以看到剛開始好好的,后面就異常了。
我們通過ThreadLocal的方式將其優(yōu)化一下。代碼如下:
/** * @author 公眾號:程序員老貓 * @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(); }}
運行了一下,完全正常了。
Thu Feb 01 23:34:30 CST 2024Thu Feb 01 23:34:30 CST 2024Thu Feb 01 23:34:30 CST 2024Thu Feb 01 23:34:30 CST 2024Thu Feb 01 23:34:30 CST 2024Thu Feb 01 23:34:30 CST 2024Thu Feb 01 23:34:30 CST 2024Thu Feb 01 23:34:30 CST 2024Thu Feb 01 23:34:30 CST 2024
那么我們什么時候會用到ThreadLocal呢?
上面針對SimpleDateFormat的封裝也算是一個吧。
如果大家還能想到其他使用的場景也歡迎留言。
上述其實咱們聊得相對而言還是比較淺的。那么接下來,咱們豐富一下之前提到的結(jié)構(gòu)圖,從源代碼側(cè)深度剖一下ThreadLocal吧。
深度結(jié)構(gòu)圖
對應上述圖中,解釋一下。
對應的我們看一下Thread的源代碼,如下:
public class Thread implements Runnable { ... ThreadLocal.ThreadLocalMap threadLocals = null; ...}
在源碼中threadLocals的初始值為Null。
抽絲剝繭,咱們繼續(xù)看一下ThreadLocalMap在調(diào)用構(gòu)造函數(shù)進行初始化的源代碼:
static class ThreadLocalMap { private static final int INITIAL_CAPACITY = 16; //初始化容量 private Entry[] table; //ThreadLocalMap數(shù)據(jù)真正存儲在table中 private int size = 0; //ThreadLocalMap條數(shù) private int threshold; // 默認為0,達到這個大小,則擴容 //類Entry的實現(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默認值為16 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //key和16取得哈希值 table[i] = new Entry(firstKey, firstValue);//創(chuàng)建節(jié)點,設置key-value size = 1; setThreshold(INITIAL_CAPACITY); //設置擴容閾值 } }
在源碼中涉及比較核心的還有set,get以及remove方法。我們依次來看一下:
set方法如下:
public void set(T value) { Thread t = Thread.currentThread(); //獲取當前線程t ThreadLocalMap map = getMap(t); //根據(jù)當前線程獲取到ThreadLocalMap if (map != null) //如果獲取的ThreadLocalMap對象不為空 map.set(this, value); //K,V設置到ThreadLocalMap中 else createMap(t, value); //創(chuàng)建一個新的ThreadLocalMap } ThreadLocalMap getMap(Thread t) { return t.threadLocals; //返回Thread對象的ThreadLocalMap屬性 } void createMap(Thread t, T firstValue) { //調(diào)用ThreadLocalMap的構(gòu)造函數(shù) t.threadLocals = new ThreadLocalMap(this, firstValue); //this表示當前類ThreadLocal }
get方法如下:
public T get() { //1、獲取當前線程 Thread t = Thread.currentThread(); //2、獲取當前線程的ThreadLocalMap ThreadLocalMap map = getMap(t); //3、如果map數(shù)據(jù)不為空, if (map != null) { //3.1、獲取threalLocalMap中存儲的值 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方法呢?其實這里會涉及到內(nèi)存泄漏的問題了。后面咱們細看。
對照著上述的結(jié)構(gòu)圖以及源碼,如果面試官問ThreadLocal原理的時候,相信大家應該可以說出個所以然來。
(1) 造成內(nèi)存泄漏的原因
這個問題其實還是得從ThreadLocal底層源碼的實現(xiàn)去看。高并發(fā)場景下,如果對ThreadLocal處理得當?shù)脑捚鋵嵕筒粫斐蓛?nèi)存泄漏。我們看下面這樣一組源代碼片段:
static class ThreadLocalMap { ... //類Entry的實現(xiàn) static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... }
上文中其實我們已經(jīng)知道Entry中以key和value的形式存儲,key是ThreadLocal本身,上面代碼中我們看到entry進行key設置的時候用的是super(k)。那就意味著調(diào)用的父類的方法去設置了key,我們再看一下父類是什么,父類其實是WeakReference。關于WeakReference底層的實現(xiàn),大家有興趣可以展開去看看源代碼,老貓在這里直接說結(jié)果。
WeakReference 如字面意思,弱引用,當一個對象僅僅被weak reference(弱引用)指向, 而沒有任何其他strong reference(強引用)指向的時候, 如果這時GC運行, 那么這個對象就會被回收,不論當前的內(nèi)存空間是否足夠,這個對象都會被回收。
關于這些引用的強弱,稍微聊一下,這里其實涉及到jvm的回收機制。在JDK1.2之后,java對引用的概念其實做了擴充的,分為強引用,軟引用,弱引用,虛引用。
明白這些概念之后,咱們再看看上面的源代碼,我們就會發(fā)現(xiàn),原來Key其實是弱引用,而里面的value因為是直接賦值行為所以是強引用。
如下圖:
jvm存儲
圖中我們可以看到由于threadLocal對象是弱引用,如果外部沒有強引用指向的話,它就會被GC回收,那么這個時候?qū)е翬ntry的key就為NULL,如果此時value外部也沒有強引用指向的話,那么這個value就永遠無法訪問了,按道理也該被回收。但是由于entry還在強引用value(看源代碼)。那么此時value就無法被回收,此時內(nèi)存泄漏就出現(xiàn)了。本質(zhì)原因是因為value成為了一個永遠無法被訪問也無法被回收的對象。
那肯定有小伙伴會有疑問了,線程本身生命周期不是很短么,如果短時間內(nèi)被銷毀,就不會內(nèi)存泄漏了,因為只要線程銷毀,那么value也會被回收。這話是沒錯。但是咱們的線程是計算機珍貴資源,為了避免重復創(chuàng)建線程帶來開銷,系統(tǒng)中我們往往會使用線程池(線程池傳送門),如果使用線程池的話,那么線程的生命周期就被拉長了,那么就可想而知了。
(2) 如何避免
解法如下:
不過話說回來,其實ThreadLocal內(nèi)部也做了優(yōu)化的。在set()的時候也會采樣清理,擴容的時候也會檢查(這里希望大家自己深入看一下源代碼),在get()的時候,如果沒有直接命中或者向后環(huán)形查找的時候也會進行清理。但是為了系統(tǒng)的穩(wěn)健萬無一失,所以大家盡量還是將上面的兩個注意點在寫代碼的時候注意下。
面試的時候大家總會去背一些八股文,但是這種也只是臨時應付面試官而已,真正的懂其中的原理才是硬道理。無論咋問,萬變不離核心原理。當然這些核心原理在我們的日常編碼中也會給我們帶來很大的幫助,用法很簡單,翻車了如何處理,那還不是得知其所以然么,伙伴們,你們覺得呢?
本文鏈接:http://www.www897cc.com/showinfo-26-73321-0.html服了,一個ThreadLocal被問出了花
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
下一篇: C++ 17 新特性,編程藝術再進化!