面試的時候被問到ThreadLocal的相關知識,沒有回答好(奶奶的,如今感受問啥都能被問倒),因此我決定先解決這幾回面試中都遇到的高頻問題,把這幾個硬骨頭都能理解的透徹的說出來了,感受最起碼不能老是一輪遊。java
ThreadLocal是JDK1.2開始就提供的一個用來存儲線程本地變量的類。ThreadLocal中的變量是在每一個線程中獨立存在的,當多個線程訪問ThreadLocal中的變量的時候,其實都是訪問的本身當前線程的內存中的變量,從而保證的變量的線程安全。面試
咱們通常在使用ThreadLocal的時候都是爲了解決線程中存在的變量競爭問題。其實解決這類問題,一般你們也會想到使用synchronized來加鎖解決。數組
例如在解決SimpleDateFormat的線程安全的時候。SimpleDateFormat是非線程安全的,它裏面不管的是format()方法仍是parse()方法,都有使用它本身內部的一個Calendar類的對象,format方法是設置時間,parse()方法裏面是先調用Calendar的clear()方法,而後又調用了Calendar的set()方法(賦值),若是一個線程剛調用了set()進行賦值,這個時候又來了一個線程直接調用了clear()方法,那麼這個parse()方法執行的結果就會有問題的。
解決辦法一
將使用SimpleDateformat的方法加上synchronized,這樣雖然保證了線程安全,但卻下降了效率,同一時間只有一個線程能使用格式化時間的方法。安全
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static synchronized String formatDate(Date date){ return simpleDateFormat.format(date); }
解決辦法二
將SimpleDateFormat的對象,放到ThreadLocal裏面,這樣每一個線程中都有一個本身的格式對象的副本了。互不干擾,從而保證了線程安全。數據結構
private static final ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static String formatDate(Date date){ return simpleDateFormatThreadLocal.get().format(date); }
咱們先看一下ThreadLocal是怎麼使用的。ide
ThreadLocal<Integer> threadLocal99 = new ThreadLocal<Integer>(); threadLocal99.set(3); int num = threadLocal99.get(); System.out.println("數字:"+num); threadLocal99.remove(); System.out.println("數字Empty:"+threadLocal99.get());
運行結果:this
數字:3 數字Empty:null
使用起來很簡單,主要是將變量放到ThreadLocal裏面,在線程執行過程當中就能夠取到,當執行完成後在remove掉就能夠了,只要沒有調用remove()當前線程在執行過程當中都是能夠拿到變量數據的。
由於是放到了當前執行的線程中,因此ThreadLocal中的變量值只能當前線程來使用,從而保證的了線程安全(當前線程的子線程其實也是能夠獲取到的)。線程
來看一下ThreadLocal的set()方法源碼code
public void set(T value) { // 獲取當前線程 Thread t = Thread.currentThread(); // 獲取ThreadLocalMap ThreadLocal.ThreadLocalMap map = getMap(t); // ThreadLocalMap 對象是否爲空,不爲空則直接將數據放入到ThreadLocalMap中 if (map != null) map.set(this, value); else createMap(t, value); // ThreadLocalMap對象爲空,則先建立對象,再賦值。 }
咱們看到變量都是存放在了ThreadLocalMap這個變量中的。那麼ThreadLocalMap又是怎麼來的呢?orm
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
public class Thread implements Runnable { ... ... /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; ... ... }
經過上面的源碼,咱們發現ThreadLocalMap變量是當前執行線程中的一個變量,因此說,ThreadLocal中存放的數據其實都是放到了當前執行線程中的一個變量裏面了。也就是存儲在了當前的線程對象裏了,別的線程裏面是另外一個線程對象了,拿不到其餘線程對象中的數據,因此數據天然就隔離開了。
那麼ThreadLocalMap是怎麼存儲數據的呢?
ThreadLocalMap 是ThreadLocal類裏的一個內部類,雖然類的名字上帶着Map但卻沒有實現Map接口,只是結構和Map相似而已。
ThreadLocalMap內部實際上是一個Entry數組,Entry是ThreadLocalMap中的一個內部類,繼承自WeakReference,並將ThreadLocal類型的對象設置爲了Entry的Key,以及對Key設置成弱引用。
ThreadLocalMap的內部數據結構,就大概是這樣的key,value組成的Entry的數組集合。
和真正的Map仍是有區別的,沒有鏈表了,這樣在解決key的hash衝突的時候措施確定就和HashMap不同了。
一個線程中是能夠建立多個ThreadLocal對象的,多個ThreadLocal對象就會存放多個數據,那麼在ThreadLocalMap中就會以數組的形式存放這些數據。
咱們來看一下具體的ThreadLocalMap的set()方法的源碼
/** * 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) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; // 定位在數組中的位置 int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 若是當前位置不爲空,而且當前位置的key和傳過來的key相等,那麼就會覆蓋當前位置的數據 if (k == key) { e.value = value; return; } // 若是當前位置爲空,則初始化一個Entry對象,放到當前位置。 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 若是當前位置不爲空,而且當前位置的key也不等於要賦值的key ,那麼將去找下一個空位置,直接將數據放到下一個空位置處。 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
咱們從set()方法中能夠看到,處理邏輯有四步。
第一步先根據Threadlocal對象的hashcode和數組長度作與運算獲取數據應該放在當前數組中的位置。
第二步就是判斷當前位置是否爲空,爲空的話就直接初始化一個Entry對象,放到當前位置。
第三步若是當前位置不爲空,而當前位置的Entry中的key和傳過來的key同樣,那麼直接覆蓋掉當前位置的數據。
第四步若是當前位置不爲空,而且當前位置的Entry中的key和傳過來的key
也不同,那麼就會去找下一個空位置,而後將數據存放到空位置(數組超過長度後,會執行擴容的);
在get的時候也是相似的邏輯,先經過傳入的ThreadLocal的hashcode獲取在Entry數組中的位置,而後拿當前位置的Entry的Key和傳入的ThreadLocal對比,相等的話,直接把數據返回,若是不相等就去判斷和數組中的下一個值的key是否相等。。。
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); }
/** * Version of getEntry method for use when key is not found in * its direct hash slot. * * @param key the thread local object * @param i the table index for key's hash code * @param e the entry at table[i] * @return the entry associated with key, or null if no such */ 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; }
咱們上文一直說,ThreadLocal是保存在單個線程中的數據,每一個線程都有本身的數據,可是實際ThreadLocal裏面的真正的對象數據,實際上是保存在堆裏面的,而線程裏面只是存儲了對象的引用而已。
而且咱們在使用的時候一般須要在上一個線程執行的方法的上下文共享ThreadLocal中的變量。
例如個人主線程是在某個方法中執行代碼呢,可是這個方法中有一段代碼時新建立了一個線程,在這個線程裏面還使用了我這個正在執行的方法裏面的定義的ThreadLocal裏面的變量。這個時候,就是須要重新線程裏面調用外面線程的數據,這個就須要線程間共享了。這種子父線程共享數據的狀況,ThreadLocal也是支持的。
例如:
ThreadLocal threadLocalMain = new InheritableThreadLocal(); threadLocalMain.set("主線程變量"); Thread t = new Thread() { @Override public void run() { super.run(); System.out.println( "如今獲取的變量是 =" + threadLocalMain.get()); } }; t.start();
運行結果:
如今獲取的變量是 =主線程變量
上面這樣的代碼就能實現子父線程共享數據的狀況,重點是使用InheritableThreadLocal來實現的共享。
那麼它是怎麼實現數據共享的呢?
在Thread類的init()方法中有這麼一段代碼:
if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
這段代碼的意思是,在建立線程的時候,若是當前線程的inheritThreadLocals變量和父線程的inheritThreadLocals變量都不爲空的時候,會將父線程的inheritThreadLocals變量中的數據,賦給當前線程中的inheritThreadLocals變量。
上文咱們也提到過,ThreadLocal中的ThreadLocalMap裏面的Entry對象是繼承自WeakReference類的,說明Entry的key是一個弱引用。
弱引用是用來描述那些非必須的對象,弱引用的對象,只能生存到下一次垃圾收集發生爲止。當垃圾收集器開始工做,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
這個弱引用仍是ThreadLocal對象自己,因此通常在線程執行完成後,ThreadLocal對象就會變成null了,而爲null的弱引用對象,在下一次GC的時候就會被清除掉,這樣Entry的Key的內存空間就被釋放出來了,可是Entry的value還在佔用的內存,若是線程是被複用的(例如線程池中的線程),那麼這裏面的value值就會愈來愈多,最終就致使了內存泄漏。
防止內存泄漏的辦法就是在每次使用完ThreadLocal的時候都去執行如下remove()方法,就能夠把key和value的空間都釋放了。
那既然容易產生內存泄漏,爲何還要設置成弱引用的呢? 若是正常狀況下應該是強引用,可是強引用只要引用關係還在就一直不會被回收,因此若是線程被複用了,那麼Entry中的Key和Value都不會被回收,這樣就形成了Key和Value都會發生內存泄漏了。