摘要:但是還有另外的功能看的后一半代碼作用就是掃描位置之后的數(shù)組直到某一個(gè)為的位置,清除每個(gè)為的,所以使用可以降低內(nèi)存泄漏的概率。
在涉及到多線程需要共享變量的時(shí)候,一般有兩種方法:其一就是使用互斥鎖,使得在每個(gè)時(shí)刻只能有一個(gè)線程訪問該變量,好處就是便于編碼(直接使用 synchronized 關(guān)鍵字進(jìn)行同步訪問),缺點(diǎn)在于這增加了線程間的競爭,降低了效率;其二就是使用本文要講的 ThreadLocal。如果說 synchronized 是以“時(shí)間換空間”,那么 ThreadLocal 就是 “以空間換時(shí)間” —— 因?yàn)?ThreadLocal 的原理就是為每個(gè)線程都提供一個(gè)這樣的變量,使得這些變量是線程級(jí)別的變量,不同線程之間互不影響,從而達(dá)到可以并發(fā)訪問而不出現(xiàn)并發(fā)問題的目的。
首先我們來看一個(gè)客觀的事實(shí):當(dāng)一個(gè)可變對(duì)象被多個(gè)線程訪問時(shí),可能會(huì)得到非預(yù)期的結(jié)果 —— 所以先讓我們來看一個(gè)例子。在講到并發(fā)訪問的問題的時(shí)候,SimpleDateFormat 總是會(huì)被拿來當(dāng)成一個(gè)絕好的例子(從這點(diǎn)看感謝 JDK 提供了這么一個(gè)有設(shè)計(jì)缺陷的類方便我們當(dāng)成反面教材 :) )。因?yàn)?SimpleDateFormat 的 format 和 parse 方法共享從父類 DateFormat 繼承而來的 Calendar 對(duì)象:
并且在 format 和 parse 方法中都會(huì)改變這個(gè) Calendar 對(duì)象:
format 方法片段:
parse 方法片段:
就拿 format 方法來說,考慮如下的并發(fā)情景:
線程A 此時(shí)調(diào)用 calendar.setTime(date1),然后 線程A 被中斷;
接著 線程B 執(zhí)行,然后調(diào)用 calendar.setTime(date2),然后 線程B 被中斷;
接著又是 線程A 執(zhí)行,但是此時(shí)的 calendar 已經(jīng)和之前的不一致了,所以便導(dǎo)致了并發(fā)問題。
所以因?yàn)檫@個(gè)共享的 calendar 對(duì)象,SimpleDateFormat 并不是一個(gè)線程安全的類,我們寫一段代碼來測(cè)試下。
(1)定義 DateFormatWrapper 類,來包裝對(duì) SimpleDateFormat 的調(diào)用:
public class DateFormatWrapper { private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String format(Date date) { return SDF.format(date); } public static Date parse(String str) throws ParseException { return SDF.parse(str); } }
(2)然后寫一個(gè) DateFormatTest,開啟多個(gè)線程來使用 DateFormatWrapper:
public class DateFormatTest { public static void main(String[] args) throws Exception { ExecutorService threadPool = Executors.newCachedThreadPool(); // 創(chuàng)建無大小限制的線程池 List> futures = new ArrayList<>(); for (int i = 0; i < 9; i++) { DateFormatTask task = new DateFormatTask(); Future> future = threadPool.submit(task); // 將任務(wù)提交到線程池 futures.add(future); } for (Future> future : futures) { try { future.get(); } catch (ExecutionException ex) { // 運(yùn)行時(shí)如果出現(xiàn)異常則進(jìn)入 catch 塊 System.err.println("執(zhí)行時(shí)出現(xiàn)異常:" + ex.getMessage()); } } threadPool.shutdown(); } static class DateFormatTask implements Callable { @Override public Void call() throws Exception { String str = DateFormatWrapper.format( DateFormatWrapper.parse("2017-07-17 16:54:54")); System.out.printf("Thread(%s) -> %s ", Thread.currentThread().getName(), str); return null; } } }
某次運(yùn)行的結(jié)果:
可以發(fā)現(xiàn),SimpleDateFormat 在多線程共享的情況下,不僅可能會(huì)出現(xiàn)結(jié)果錯(cuò)誤的情況,還可能會(huì)由于并發(fā)訪問導(dǎo)致運(yùn)行異常。當(dāng)然,我們肯定有解決的辦法:
為 DateFormatWrapper 的 format 和 parse 方法加上 synchronized 關(guān)鍵字,壞處就是前面提到的這會(huì)加大線程間的競爭和切換而降低效率;
不使用全局的 SimpleDateFormat 對(duì)象,而是每次使用 format 和 parse 方法都新建一個(gè) SimpleDateFormat 對(duì)象,壞處也很明顯,每次調(diào)用 format 或者 parse 方法都要新建一個(gè) SimpleDateFormat,這會(huì)加大 GC 的負(fù)擔(dān);
使用 ThreadLocal。ThreadLocal
我們使用 ThreadLocal 來對(duì) DateFormatWrapper 進(jìn)行修改,使得每個(gè)線程使用多帶帶的 SimpleDateFormat:
public class DateFormatWrapper { private static final ThreadLocalSDF = new ThreadLocal () { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String format(Date date) { return SDF.get().format(date); } public static Date parse(String str) throws ParseException { return SDF.get().parse(str); } }
如果使用 Java8,則初始化 ThreadLocal 對(duì)象的代碼可以改為:
private static final ThreadLocalSDF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
然后再運(yùn)行 DateFormatTest,便始終是預(yù)期的結(jié)果:
我們已經(jīng)看到了 ThreadLocal 的功能,那 ThreadLocal 是如何實(shí)現(xiàn)為每個(gè)線程提供一份共享變量的拷貝呢?
在使用 ThreadLocal 時(shí),當(dāng)前線程訪問 ThreadLocal 中包含的變量是通過 get() 方法,所以首先來看這個(gè)方法的實(shí)現(xiàn):
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
通過代碼可以猜測(cè):
在某個(gè)地方(其實(shí)就是在 ThreadLocal 的內(nèi)部),JDK 實(shí)現(xiàn)了一個(gè)類似于 HashMap 的類,叫 ThreadLocalMap,該 “Map” 的鍵類型為 ThreadLocal
然后每個(gè)線程都關(guān)聯(lián)著一個(gè) ThreadLocalMap 對(duì)象,并且可以通過 getMap(Thread t) 方法來獲得 線程t 關(guān)聯(lián)的 ThreadLocalMap 對(duì)象;
ThreadLocalMap 類有個(gè)以 ThreadLocal 對(duì)象為參數(shù)的 getEntry(ThreadLocal) 的方法,用來獲得當(dāng)前 ThreadLocal 對(duì)象關(guān)聯(lián)的 Entry 對(duì)象。一個(gè) Entry 對(duì)象就是一個(gè)鍵值對(duì),鍵(key)是 ThreadLocal 對(duì)象,值(value)是該 ThreadLocal 對(duì)象包含的變量(即 T)。
查看 getMap(Thread) 方法:
直接返回的就是 t.threadLocals,原來在 Thread 類中有一個(gè)就叫 threadLocals 的 ThreadLocalMap 的變量:
所以每個(gè) Thread 都會(huì)擁有一個(gè) ThreadLocalMap 變量,來存放屬于該 Thread 的所有 ThreadLocal 變量。這樣來看的話,ThreadLocal就相當(dāng)于一個(gè)調(diào)度器,每次調(diào)用 get 方法的時(shí)候,都會(huì)先找到當(dāng)前線程的 ThreadLocalMap,然后再在這個(gè) ThreadLocalMap 中找到對(duì)應(yīng)的線程本地變量。
然后我們來看看當(dāng) map 為 null(即第一次調(diào)用 get())時(shí)調(diào)用的 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; }
該方法首先會(huì)調(diào)用 initialValue() 方法來獲得該 ThreadLocal 對(duì)象中需要包含的變量 —— 所以這就是為什么使用 ThreadLocal 是需要繼承 ThreadLocal 時(shí)并覆寫 initialValue() 方法,因?yàn)檫@樣才能讓 setInitialValue() 調(diào)用 initialValue() 從而得到 ThreadLocal 包含的初始變量;然后就是當(dāng) map 不為 null 的時(shí)候,將該變量(value)與當(dāng)前ThreadLocal對(duì)象(this)在 map 中進(jìn)行關(guān)聯(lián);如果 map 為 null,則調(diào)用 createMap 方法:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
createMap 會(huì)調(diào)用 ThreadLocalMap 的構(gòu)造方法來創(chuàng)建一個(gè) ThreadLocalMap 對(duì)象:
可以看到該方法通過一個(gè) ThreadLocal 對(duì)象(firstKey)和該 ThreadLocal 包含的對(duì)象(firstValue)構(gòu)造了一個(gè) ThreadLocalMap 對(duì)象,使得該 map 在構(gòu)造完畢時(shí)候就包含了這樣一個(gè)鍵值對(duì)(firstKey -> firstValue)。
為啥需要使用 Map 呢?因?yàn)橐粋€(gè)線程可能有多個(gè) ThreadLocal 對(duì)象,可能是包含 SimpleDateFormat,也可能是包含一個(gè)數(shù)據(jù)庫連接 Connection,所以不同的變量需要通過對(duì)應(yīng)的 ThreadLocal 對(duì)象來快速查找 —— 那么 Map 當(dāng)然是最好的方式。
ThreadLocal 還提供了修改和刪除當(dāng)前包含對(duì)象的方法,修改的方法為 set,刪除的方法為 remove:
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)前 ThredLocal 還沒有包含值,那么就調(diào)用 createMap 來初始化當(dāng)前線程的 ThreadLocalMap 對(duì)象,否則直接在 map 中修改當(dāng)前 ThreadLocal(this)包含的值。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
remove 方法就是獲得當(dāng)前線程的 ThreadLocalMap 對(duì)象,然后調(diào)用這個(gè) map 的 remove(ThreadLocal) 方法。查看 ThreadLocalMap 的 remove(ThreadLocal) 方法的實(shí)現(xiàn):
邏輯就是先找到參數(shù)(ThreadLocal對(duì)象)對(duì)應(yīng)的 Entry,然后調(diào)用 Entry 的 clear() 方法,再調(diào)用 expungeStaleEntry(i),i 為該 Entry 在 map 的 Entry 數(shù)組中的索引。
(1)首先來看看 e.clear() 做了什么。
查看 ThreadLocalMap 的源代碼,我們可以發(fā)現(xiàn)這個(gè) “Map” 的 Entry 的實(shí)現(xiàn)如下:
可以看到,該 Entry 類繼承自 WeakReference
我們知道對(duì)于一個(gè)弱引用的對(duì)象,一旦該對(duì)象不再被其他對(duì)象引用(比如像 clear() 方法那樣將對(duì)象引用直接設(shè)置為 null),那么在 GC 發(fā)生的時(shí)候,該對(duì)象便會(huì)被 GC 回收。所以讓 Entry 作為一個(gè) WeakReference,配合 ThreadLocal 的 remove 方法,可以及時(shí)清除某個(gè) Entry 中的 ThreadLocal(Entry 的 key)。
(2)expungeStaleEntry(i)的作用
先來看 expungeStaleEntry 的前一半代碼:
expungeStaleEntry 這部分代碼的作用就是將 i 位置上的 Entry 的 value 設(shè)置為 null,以及將 Entry 的引用設(shè)置為 null。為什么要這做呢?因?yàn)榍懊嬲{(diào)用 e.clear(),只是將 Entry 的 key 設(shè)置為 null 并且可以使其在 GC 是被快速回收,但是 Entry 的 value 在調(diào)用 e.clear() 后并不會(huì)為 null —— 所以如果不對(duì) value 也進(jìn)行清除,那么就可能會(huì)導(dǎo)致內(nèi)存泄漏了。因此expungeStaleEntry 方法的一個(gè)作用在于可以把需要清除的 Entry 徹底的從 ThreadLocalMap 中清除(key,value,Entry 全部設(shè)置為 null)。但是 expungeStaleEntry 還有另外的功能:看 expungeStaleEntry 的后一半代碼:
作用就是掃描位置 staleSlot 之后的 Entry 數(shù)組(直到某一個(gè)為 null 的位置),清除每個(gè) key(ThreadLocal) 為 null 的 Entry,所以使用 expungeStaleEntry 可以降低內(nèi)存泄漏的概率。但是如果某些 ThreadLocal 變量不需要使用但是卻沒有調(diào)用到 expungeStaleEntry 方法,那么就會(huì)導(dǎo)致這些 ThreadLocal 變量長期的貯存在內(nèi)存中,引起內(nèi)存浪費(fèi)或者泄露 —— 所以,如果確定某個(gè) ThreadLocal 變量已經(jīng)不需要使用,需要及時(shí)的使用 ThreadLocal 的 remove() 方法(ThreadLocal 的 get 和 set 方法也會(huì)調(diào)用到 expungeStaleEntry),將其從內(nèi)存中清除。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/67412.html
摘要:下面是線程相關(guān)的熱門面試題,你可以用它來好好準(zhǔn)備面試。線程安全問題都是由全局變量及靜態(tài)變量引起的。持有自旋鎖的線程在之前應(yīng)該釋放自旋鎖以便其它線程可以獲得自旋鎖。 最近看到網(wǎng)上流傳著,各種面試經(jīng)驗(yàn)及面試題,往往都是一大堆技術(shù)題目貼上去,而沒有答案。 不管你是新程序員還是老手,你一定在面試中遇到過有關(guān)線程的問題。Java語言一個(gè)重要的特點(diǎn)就是內(nèi)置了對(duì)并發(fā)的支持,讓Java大受企業(yè)和程序員...
摘要:基本原理線程本地變量是和線程相關(guān)的變量,一個(gè)線程則一份數(shù)據(jù)。其中為聲明的對(duì)象。對(duì)于一個(gè)對(duì)象倘若沒有成員變量,單例非常簡單,不用去擔(dān)心多線程同時(shí)對(duì)成員變量修改而產(chǎn)生的線程安全問題。并且還不能使用單例模式,因?yàn)槭遣荒芏嗑€程訪問的。 ThreadLocal簡述 下面我們看一下ThreadLocal類的官方注釋。 This class provides thread-local variab...
摘要:理解內(nèi)存模型對(duì)多線程編程無疑是有好處的。干貨高級(jí)動(dòng)畫高級(jí)動(dòng)畫進(jìn)階,矢量動(dòng)畫。 這是最好的Android相關(guān)原創(chuàng)知識(shí)體系(100+篇) 知識(shí)體系從2016年開始構(gòu)建,所有的文章都是圍繞著這個(gè)知識(shí)體系來寫,目前共收入了100多篇原創(chuàng)文章,其中有一部分未收入的文章在我的新書《Android進(jìn)階之光》中。最重要的是,這個(gè)知識(shí)體系仍舊在成長中。 Android 下拉刷新庫,這一個(gè)就夠了! 新鮮出...
摘要:如問到是否使用某框架,實(shí)際是是問該框架的使用場(chǎng)景,有什么特點(diǎn),和同類可框架對(duì)比一系列的問題。這兩個(gè)方向的區(qū)分點(diǎn)在于工作方向的側(cè)重點(diǎn)不同。 [TOC] 這是一份來自嗶哩嗶哩的Java面試Java面試 32個(gè)核心必考點(diǎn)完全解析(完) 課程預(yù)習(xí) 1.1 課程內(nèi)容分為三個(gè)模塊 基礎(chǔ)模塊: 技術(shù)崗位與面試 計(jì)算機(jī)基礎(chǔ) JVM原理 多線程 設(shè)計(jì)模式 數(shù)據(jù)結(jié)構(gòu)與算法 應(yīng)用模塊: 常用工具集 ...
閱讀 2556·2021-09-22 15:25
閱讀 2963·2021-09-14 18:03
閱讀 1212·2021-09-09 09:33
閱讀 1700·2021-09-07 09:59
閱讀 2930·2021-07-29 13:50
閱讀 1500·2019-08-30 15:44
閱讀 1715·2019-08-29 16:22
閱讀 1287·2019-08-29 12:49