簡介:本文以一個簡要的代碼示例介紹ThreadLocal類的基本使用,在此基礎上結合圖片闡述它的內部工做原理,最後分析了ThreadLocal的內存泄露問題以及解決方法。html
歡迎探討,若有錯誤敬請指正 java
如需轉載,請註明出處 http://www.cnblogs.com/nullzx/編程
ThreadLocal只有一個無參的構造方法併發
public ThreadLocal()
ThreadLocal的相關方法dom
public T get() public void set(T value) public void remove() protected T initialValue()
initialValue方法的訪問修飾符是protected,該方法爲第一次調用get方法提供一個初始值。默認狀況下,第一次調用get方法返回值null。在使用時,咱們通常會複寫ThreadLocal的initialValue方法,使第一次調用get方法時返回一個咱們設定的初始值。ide
下面是一個ThreadLocal的一個簡單使用示例函數
package javalearning; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; public class ThreadLocalDemo { /*定義了1個ThreadLocal<Integer>對象, *並複寫它的initialValue方法,初始值是3*/ private ThreadLocal<Integer> tlA = new ThreadLocal<Integer>(){ protected Integer initialValue(){ return 3; } }; /* private ThreadLocal<Integer> tlB = new ThreadLocal<Integer>(){ protected Integer initialValue(){ return 5; } }; */ /*設置一個信號量,許可數爲1,讓三個線程順序執行*/ Semaphore semaphore = new Semaphore(1); private Random rnd = new Random(); /*Worker定義爲內部類實現了Runnable接口,tlA定義在外部類中, 每一個線程中調用這個對象的get方法,再調用一個set方法設置一個隨機值*/ public class Worker implements Runnable{ @Override public void run(){ try { Thread.sleep(rnd.nextInt(1000)); /*隨機延時1s之內的時間*/ semaphore.acquire();/*獲取許可*/ } catch (InterruptedException e) { e.printStackTrace(); } int valA = tlA.get(); System.out.println(Thread.currentThread().getName() +" tlA initial val : "+ valA); valA = rnd.nextInt(); tlA.set(valA); System.out.println(Thread.currentThread().getName() +" tlA new val: "+ valA); /* int valB = tlB.get(); System.out.println(Thread.currentThread().getName() +" tlB initial val : "+ valB); valB = rnd.nextInt(); tlA.set(valB); System.out.println(Thread.currentThread().getName() +" tlB 2 new val: "+ valB); */ semaphore.release(); /*在線程池中,當線程退出以前必定要記得調用remove方法,由於在線程池中的線程對象是循環使用的*/ tlA.remove(); /*tlB.remove();*/ } } /*建立三個線程,每一個線程都會對ThreadLocal對象tlA進行操做*/ public static void main(String[] args){ ExecutorService es = Executors.newFixedThreadPool(3); ThreadLocalDemo tld = new ThreadLocalDemo(); es.execute(tld.new Worker()); es.execute(tld.new Worker()); es.execute(tld.new Worker()); es.shutdown(); } }
運行結果學習
pool-1-thread-1 tlA initial val : 3 pool-1-thread-1 tlA new val: -1288455998 pool-1-thread-3 tlA initial val : 3 pool-1-thread-3 tlA new val: 112537197 pool-1-thread-2 tlA initial val : 3 pool-1-thread-2 tlA new val: -12271334
從運行結果能夠看出,每一個線程第一次調用TheadLocal對象的get方法時都獲得初始值3,注意咱們上面的代碼是讓三個線程順序執行,顯然從運行結果看,pool-1-thread-1線程結束後設置的新值,對pool-1-thread-3線程是沒有影響的,pool-1-thread-3線程完成後設置的新值對pool-1-thread-2線程也沒有影響。這就彷彿把ThreadLocal對象當作每一個線程內部的對象同樣,但實際上tlA對象是個外部類對象,內部類Worker訪問到的是同一個tlA對象,也就是說是被各個線程共享的。這是如何作到的呢?咱們如今就來看看ThreadLocal對象的內部原理。ui
首先,在Thread類中定義了一個threadLocals,它是ThreadLocal.ThreadLocalMap對象的引用,默認值是null。ThreadLocal.ThreadLocalMap對象表示了一個以開放地址形式的散列表。當咱們在線程的run方法中第一次調用ThreadLocal對象的get方法時,會爲當前線程建立一個ThreadLocalMap對象。也就是每一個線程都各自有一張獨立的散列表,以ThreadLocal對象做爲散列表的key,set方法中的值做爲value(第一次調用get方法時,以initialValue方法的返回值做爲value)。顯然咱們能夠定義多個ThreadLocal對象,而咱們通常將ThreadLocal對象定義爲static類型或者外部類中。上面所表達的意思就是,相同的key在不一樣的散列表中的值必然是獨立的,每一個線程都是在各自的散列表中執行操做。this
TheadLocal中的get源代碼
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);//這裏的this是指當前的ThreadLocal對象 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
ThreadLocalMap 中用內部靜態類Entry表示了散列表中的每個條目,下面是它的代碼
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
能夠看出Entry類繼承了WeakRefrence類,因此一個條目就是一個弱引用類型的對象(要搞清楚,持有weakRefrence對象的引用是個強引用),那麼這個weakRefrence對象保存了誰的弱引用呢?咱們看到構造函數中有個supe(k),k是ThreadLocal類型對象,super表示是調用父類的構造函數(父類是誰你要想清楚哦?),因此說一個entry對象中存儲了ThreadLocal對象的弱引用和這個ThreadLocal對應的value對象的強引用。有關弱引用的相關內容請參考個人另外一篇博客《Java中的四種引用以及ReferenceQueue和WeakHashMap的使用示例》。
當這個方法結束時,這個方法中建立的ThreadLocal對象自己(圖中綠色區域)就被垃圾回收器回收了,可是線程尚未結束,因此ThreadLocalMap中還存在這個entry。因爲entry中的key(即ThreadLocal對象)是弱引用類型,因此此時調用entry.get()方法時就會返回null,內部結構以下圖所示。
從圖中咱們能夠看到value對象(紅色區域)始終不能被回收,而咱們不再會使用它了,這就形成了內存泄露。
那Entry中爲何保存的是key的弱引用呢?其實這是爲了最大程度上減小內存泄露,反作用是同時減小哈希表中的衝突。當ThreadLocal對象被回收時,對應entry中的key就自動變成null(entry對象自己不爲null)。若此後咱們調用get,set或remove方法時,就會嘗試刪除key爲null的entry,以釋放value對象所佔用的內存。
咱們如今來看看get方法(上面有get方法的源代碼)中調用的getEntry方法。
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); }
從源代碼中咱們能夠看出,有可能會調用getEntryAfterMiss方法,而在這個方法中,刪除key爲null的Entry對象。同理set方法也有相似的行爲,而remove方法不只僅刪除掉參數ThreadLocal對象對應的entry,並且也會嘗試刪除其它key爲null的entry。
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; }
可是上述的方式並不能徹底解決內存泄露問題,由於咱們在這個方法結束的時候邏輯上不必定必須調用get方法,而get方法也不必定執行getEntryAfterMiss方法。因此類自己是沒有這個能力的,咱們只能在再也不使用某個ThreadLocal對象後,手動調用remoev方法來刪除它,各自線程中調用共享的ThreadLocal對象的remove方法,這對其它線程是沒有影響的,這個應該不難理解。在線程池中這就操做是必須的,不只僅是內存泄露的問題。由於線程池中的線程是重複使用的,意味着這個線程的ThreadLocalMap對象也是重複使用的,若是咱們不手動調用remove方法,那麼後面的線程就有可能獲取到上個線程遺留下來的value值,形成bug。
4. 參考內容