ThreadLocal原理及使用示例

簡介:本文以一個簡要的代碼示例介紹ThreadLocal類的基本使用,在此基礎上結合圖片闡述它的內部工做原理,最後分析了ThreadLocal的內存泄露問題以及解決方法。html

 

歡迎探討,若有錯誤敬請指正 java

如需轉載,請註明出處 http://www.cnblogs.com/nullzx/編程


1. ThreadLocal<T> 簡介和使用示例

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

 

2. ThreadLocal<T>的原理

首先,在Thread類中定義了一個threadLocals,它是ThreadLocal.ThreadLocalMap對象的引用,默認值是null。ThreadLocal.ThreadLocalMap對象表示了一個以開放地址形式的散列表。當咱們在線程的run方法中第一次調用ThreadLocal對象的get方法時,會爲當前線程建立一個ThreadLocalMap對象。也就是每一個線程都各自有一張獨立的散列表,以ThreadLocal對象做爲散列表的key,set方法中的值做爲value(第一次調用get方法時,以initialValue方法的返回值做爲value)。顯然咱們能夠定義多個ThreadLocal對象,而咱們通常將ThreadLocal對象定義爲static類型或者外部類中。上面所表達的意思就是,相同的key在不一樣的散列表中的值必然是獨立的,每一個線程都是在各自的散列表中執行操做this

ThreadLocal_thumb4

 

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();
    }

 3. 由ThreadLocal形成的內存泄露和相應解決辦法

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的使用示例》

咱們如今假設一種狀況,假設咱們在線程的run方法中調用了一個方法,並在這個方法中建立了ThreadLocal對象,並使用了他,內存結構示意圖以下。

image

當這個方法結束時,這個方法中建立的ThreadLocal對象自己(圖中綠色區域)就被垃圾回收器回收了,可是線程尚未結束,因此ThreadLocalMap中還存在這個entry。因爲entry中的key(即ThreadLocal對象)是弱引用類型,因此此時調用entry.get()方法時就會返回null,內部結構以下圖所示。

image

從圖中咱們能夠看到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. 參考內容

[1]. Java併發編程:深刻剖析ThreadLocal

[2]. [Java併發包學習七]解密ThreadLocal

[3]. 深刻分析 ThreadLocal 內存泄漏問題

相關文章
相關標籤/搜索