在涉及到多線程須要共享變量的時候,通常有兩種方法:其一就是使用互斥鎖,使得在每一個時刻只能有一個線程訪問該變量,好處就是便於編碼(直接使用 synchronized
關鍵字進行同步訪問),缺點在於這增長了線程間的競爭,下降了效率;其二就是使用本文要講的 ThreadLocal
。若是說 synchronized
是以「時間換空間」,那麼 ThreadLocal
就是 「以空間換時間」 —— 由於 ThreadLocal
的原理就是爲每一個線程都提供一個這樣的變量,使得這些變量是線程級別的變量,不一樣線程之間互不影響,從而達到能夠併發訪問而不出現併發問題的目的。數據庫
首先咱們來看一個客觀的事實:當一個可變對象被多個線程訪問時,可能會獲得非預期的結果 —— 因此先讓咱們來看一個例子。在講到併發訪問的問題的時候,SimpleDateFormat
老是會被拿來當成一個絕好的例子(從這點看感謝 JDK 提供了這麼一個有設計缺陷的類方便咱們當成反面教材 :) )。由於 SimpleDateFormat
的 format
和 parse
方法共享從父類 DateFormat
繼承而來的 Calendar
對象:
數組
而且在 format
和 parse
方法中都會改變這個 Calendar
對象:安全
format
方法片斷:
parse
方法片斷:
就拿 format
方法來講,考慮以下的併發情景:多線程
calendar.setTime(date1)
,而後 線程A 被中斷;calendar.setTime(date2)
,而後 線程B 被中斷;calendar
已經和以前的不一致了,因此便致使了併發問題。因此由於這個共享的 calendar
對象,SimpleDateFormat
並非一個線程安全的類,咱們寫一段代碼來測試下。併發
(1)定義 DateFormatWrapper
類,來包裝對 SimpleDateFormat
的調用:app
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
:ide
public class DateFormatTest { public static void main(String[] args) throws Exception { ExecutorService threadPool = Executors.newCachedThreadPool(); // 建立無大小限制的線程池 List<Future<?>> 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) { // 運行時若是出現異常則進入 catch 塊 System.err.println("執行時出現異常:" + ex.getMessage()); } } threadPool.shutdown(); } static class DateFormatTask implements Callable<Void> { @Override public Void call() throws Exception { String str = DateFormatWrapper.format( DateFormatWrapper.parse("2017-07-17 16:54:54")); System.out.printf("Thread(%s) -> %s\n", Thread.currentThread().getName(), str); return null; } } }
某次運行的結果:
測試
能夠發現,SimpleDateFormat
在多線程共享的狀況下,不只可能會出現結果錯誤的狀況,還可能會因爲併發訪問致使運行異常。固然,咱們確定有解決的辦法:this
DateFormatWrapper
的 format
和 parse
方法加上 synchronized
關鍵字,壞處就是前面提到的這會加大線程間的競爭和切換而下降效率;SimpleDateFormat
對象,而是每次使用 format
和 parse
方法都新建一個 SimpleDateFormat
對象,壞處也很明顯,每次調用 format
或者 parse
方法都要新建一個 SimpleDateFormat
,這會加大 GC 的負擔;ThreadLocal
。ThreadLocal<SimpleDateFormat>
能夠爲每一個線程提供一個獨立的 SimpleDateFormat
對象,建立的 SimpleDateFormat
對象個數最多和線程個數相同,相比於 (1),使用ThreadLocal
不存在線程間的競爭;相比於 (2),使用ThreadLocal
建立的 SimpleDateFormat
對象個數也更加合理(不會超過線程的數量)。咱們使用 ThreadLocal
來對 DateFormatWrapper
進行修改,使得每一個線程使用單獨的 SimpleDateFormat
:編碼
public class DateFormatWrapper { private static final ThreadLocal<SimpleDateFormat> SDF = new ThreadLocal<SimpleDateFormat>() { @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 ThreadLocal<SimpleDateFormat> SDF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
而後再運行 DateFormatTest
,便始終是預期的結果:
咱們已經看到了 ThreadLocal
的功能,那 ThreadLocal
是如何實現爲每一個線程提供一份共享變量的拷貝呢?
在使用 ThreadLocal
時,當前線程訪問 ThreadLocal
中包含的變量是經過 get()
方法,因此首先來看這個方法的實現:
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 實現了一個相似於 HashMap
的類,叫 ThreadLocalMap
,該 「Map」 的鍵類型爲 ThreadLocal<T>
,值類型爲 T
;ThreadLocalMap
對象,而且能夠經過 getMap(Thread t)
方法來得到 線程t 關聯的 ThreadLocalMap
對象;ThreadLocalMap
類有個以 ThreadLocal
對象爲參數的 getEntry(ThreadLocal)
的方法,用來得到當前 ThreadLocal
對象關聯的 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 中進行關聯;若是 map 爲 null
,則調用 createMap
方法:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
createMap
會調用 ThreadLocalMap
的構造方法來建立一個 ThreadLocalMap
對象:
能夠看到該方法經過一個 ThreadLocal
對象(firstKey)和該 ThreadLocal
包含的對象(firstValue)構造了一個 ThreadLocalMap
對象,使得該 map 在構造完畢時候就包含了這樣一個鍵值對(firstKey -> firstValue)。
爲啥須要使用 Map 呢?由於一個線程可能有多個 ThreadLocal
對象,多是包含 SimpleDateFormat
,也多是包含一個數據庫鏈接 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)
方法的實現:
邏輯就是先找到參數(ThreadLocal
對象)對應的 Entry
,而後調用 Entry
的 clear()
方法,再調用 expungeStaleEntry(i)
,i 爲該 Entry
在 map 的 Entry
數組中的索引。
(1)首先來看看 e.clear()
作了什麼。
查看 ThreadLocalMap
的源代碼,咱們能夠發現這個 「Map」 的 Entry
的實現以下:
能夠看到,該 Entry
類繼承自 WeakReference<ThreadLocal<?>>
,因此 Entry
是一個 WeakReference
(弱引用),並且該 WeakReference
包含的是一個 ThreadLocal
對象 —— 於是每一個 Entry
是一個弱引用的 ThreadLocal
對象(又由於 Entry
包括了一個 value 變量,因此該 Entry
構成了一個 ThreadLocal -> Object
的鍵值對),而 Entry
的 clear()
方法,是繼承自 WeakReference
,做用就是將 WeakReference
包含的對象的引用設置爲 null
:
咱們知道對於一個弱引用的對象,一旦該對象再也不被其餘對象引用(好比像 clear()
方法那樣將對象引用直接設置爲 null
),那麼在 GC 發生的時候,該對象便會被 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
數組(直到某一個爲 null
的位置),清除每一個 key(ThreadLocal
) 爲 null
的 Entry
,因此使用 expungeStaleEntry
能夠下降內存泄漏的機率。可是若是某些 ThreadLocal
變量不須要使用可是卻沒有調用到 expungeStaleEntry
方法,那麼就會致使這些 ThreadLocal
變量長期的貯存在內存中,引發內存浪費或者泄露 —— 因此,若是肯定某個 ThreadLocal
變量已經不須要使用,須要及時的使用 ThreadLocal
的 remove()
方法(ThreadLocal
的 get
和 set
方法也會調用到 expungeStaleEntry
),將其從內存中清除。