之前面試的時候問到ThreadLocal老是一臉懵逼,只知道有這個哥們,不瞭解他是用來作什麼的,更不清楚他的原理了。表面上看他是和多線程,線程同步有關的一個工具類,但其實他與線程同步機制無關。java
線程同步機制是多個線程共享同一個變量,而ThreadLocal是爲每一個線程建立一個單獨的變量副本,每一個線程均可以改變本身的變量副本而不影響其它線程所對應的副本。面試
官方API上是這樣介紹的:api
該類提供了線程局部(thread-local)變量。這些變量不一樣於它們的普通對應物,由於訪問某個變量(經過其 get 或 set 方法)的每一個線程都有本身的局部變量,它獨立於變量的初始化副本。ThreadLocal實例一般是類中的 private static 字段,它們但願將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。數組
ThreadLocal定義了四個方法:安全
ThreadLocal還有一個特別重要的靜態內部類ThreadLocalMap,該類纔是實現線程隔離機制的關鍵。get()、set()、remove()都是基於該內部類進行操做,ThreadLocalMap用鍵值對方式存儲每一個線程變量的副本,key爲當前的ThreadLocal對象,value爲對應線程的變量副本。多線程
試想,每一個線程都有本身的ThreadLocal對象,也就是都有本身的ThreadLocalMap,對本身的ThreadLocalMap操做,固然是互不影響的了,這就不存在線程安全問題了,因此ThreadLocal是以空間來交換安全性的解決思路。ide
假設每一個線程都須要一個計數值記錄本身作某件事作了多少次,各線程運行時都須要改變本身的計數值並且相互不影響,那麼ThreadLocal就是很好的選擇,這裏ThreadLocal裏保存的當前線程的局部變量的副本就是這個計數值。工具
public class SeqCount { private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } }; public int nextSeq() { seqCount.set(seqCount.get() +1); return seqCount.get(); } public static void main(String [] args) { SeqCount seqCount = new SeqCount(); SeqThread seqThread1 = new SeqThread(seqCount); SeqThread seqThread2 = new SeqThread(seqCount); SeqThread seqThread3 = new SeqThread(seqCount); SeqThread seqThread4 = new SeqThread(seqCount); seqThread1.start(); seqThread2.start(); seqThread3.start(); seqThread4.start(); } public static class SeqThread extends Thread { private SeqCount seqCount; public SeqThread(SeqCount seqCount) { this.seqCount = seqCount; } @Override public void run() { for (int i=0; i<3; i++) { System.out.println(Thread.currentThread().getName()+" seqCount:"+seqCount.nextSeq()); } } } }
運行結果:
源碼分析
咱們知道SimpleDateFormat在多線程下是存在線程安全問題的,那麼將SimpleDateFormat做爲每一個線程的局部變量的副本就是每一個線程都擁有本身的SimpleDateFormat,就不存在線程安全問題了。this
public class SimpleDateFormatDemo { private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<>(); /** * 獲取線程的變量副本,若是不覆蓋initialValue方法,第一次get將返回null,故須要建立一個DateFormat,放入threadLocal中 * @return */ public DateFormat getDateFormat() { DateFormat df = threadLocal.get(); if (df == null) { df = new SimpleDateFormat(DATE_FORMAT); threadLocal.set(df); } return df; } public static void main(String [] args) { SimpleDateFormatDemo formatDemo = new SimpleDateFormatDemo(); MyRunnable myRunnable1 = new MyRunnable(formatDemo); MyRunnable myRunnable2 = new MyRunnable(formatDemo); MyRunnable myRunnable3 = new MyRunnable(formatDemo); Thread thread1= new Thread(myRunnable1); Thread thread2= new Thread(myRunnable2); Thread thread3= new Thread(myRunnable3); thread1.start(); thread2.start(); thread3.start(); } public static class MyRunnable implements Runnable { private SimpleDateFormatDemo dateFormatDemo; public MyRunnable(SimpleDateFormatDemo dateFormatDemo) { this.dateFormatDemo = dateFormatDemo; } @Override public void run() { System.out.println(Thread.currentThread().getName()+" 當前時間:"+dateFormatDemo.getDateFormat().format(new Date())); } } }
運行結果:
ThreadLocalMap內部是利用Entry來進行key-value的存儲的。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
上面源碼中key就是ThreadLocal,value就是值,Entry繼承WeakReference,因此Entry對應key的引用(ThreadLocal實例)是一個弱引用。
/** * Set the value associated with key. * * @param key the thread local object * @param value the value to be set */ private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; //根據ThreadLocal的散列值,查找對應元素在數組中的位置 int i = key.threadLocalHashCode & (len-1); //採用線性探測法尋找合適位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //key存在,直接覆蓋 if (k == key) { e.value = value; return; } // key == null,可是存在值(由於此處的e != null),說明以前的ThreadLocal對象已經被回收了 if (k == null) { replaceStaleEntry(key, value, i); return; } } //ThreadLocal對應的key實例不存在,new一個 tab[i] = new Entry(key, value); int sz = ++size; //清楚陳舊的Entry(key == null的) // 若是沒有清理陳舊的 Entry 而且數組中的元素大於了閾值,則進行 rehash if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
這個set操做和集合Map解決散列衝突的方法不一樣,集合Map採用的是鏈地址法,這裏採用的是開放定址法(線性探測)。set()方法中的replaceStaleEntry()和cleanSomeSlots(),這兩個方法能夠清除掉key ==null的實例,防止內存泄漏。
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
因爲採用了開放定址法,當前keu的散列值和元素在數組中的索引並非一一對應的,首先取一個猜想數(key的散列值),若是所對應的key是咱們要找的元素,那麼直接返回,不然調用getEntryAfterMiss
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
這裏一直在探測尋找下一個元素,知道找的元素的key是咱們要找的。這裏當key==null時,調用expungeStaleEntry有利於GC的回收,用於防止內存泄漏。
ThreadLocalMap的key爲ThreadLocal實例,他是一個弱引用,咱們知道弱引用有利於GC的回收,當key == null時,GC就會回收這部分空間,但value不必定能被回收,由於他和Current Thread之間還存在一個強引用的關係。
因爲這個強引用的關係,會致使value沒法回收,若是線程對象不消除這個強引用的關係,就可能會出現OOM。有些時候,咱們調用ThreadLocalMap的remove()方法進行顯式處理。
ThreadLocal不是用來解決共享變量的問題,也不是協調線程同步,他是爲了方便各線程管理本身的狀態而引用的一個機制。
每一個ThreadLocal內部都有一個ThreadLocalMap,他保存的key是ThreadLocal的實例,他的值是當前線程的局部變量的副本的值。