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