深刻理解ThreadLocal

學習一個東西首先要知道爲何要引入它,就是咱們能用它來幹什麼。因此咱們先來看看ThreadLocal對咱們到底有什麼用,而後再來看看它的實現原理。 java

ThreadLocal若是單純從名字上來看像是「本地線程"這麼個意思,只能說這個名字起的確實不太好,很容易讓人產生誤解,ThreadLocalVariable(線程本地變量)應該是個更好的名字。咱們先看一下官方對ThreadLocal的描述: ide

該類提供了線程局部 (thread-local) 變量。這些變量不一樣於它們的普通對應物,由於訪問某個變量(經過其 get 或 set 方法)的每一個線程都有本身的局部變量,它獨立於變量的初始化副本。ThreadLocal 實例一般是類中的 private static 字段,它們但願將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。
咱們從中摘出要點:


一、每一個線程都有本身的局部變量 函數

    每一個線程都有一個獨立於其餘線程的上下文來保存這個變量,一個線程的本地變量對其餘線程是不可見的(有前提,後面解釋) 學習

二、獨立於變量的初始化副本 this

    ThreadLocal能夠給一個初始值,而每一個線程都會得到這個初始化值的一個副本,這樣才能保證不一樣的線程都有一份拷貝。 spa

三、狀態與某一個線程相關聯 線程

    ThreadLocal 不是用於解決共享變量的問題的,不是爲了協調線程同步而存在,而是爲了方便每一個線程處理本身的狀態而引入的一個機制,理解這點對正確使用ThreadLocal相當重要。 code

咱們先看一個簡單的例子: 對象

public class ThreadLocalTest {
        
        //建立一個Integer型的線程本地變量
	public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return 0;
		}
	};
	public static void main(String[] args) throws InterruptedException {
		Thread[] threads = new Thread[5];
		for (int j = 0; j < 5; j++) {		
               threads[j] = new Thread(new Runnable() {
				@Override
				public void run() {
                                        //獲取當前線程的本地變量,而後累加5次
					int num = local.get();
					for (int i = 0; i < 5; i++) {
						num++;
					}
                                        //從新設置累加後的本地變量
					local.set(num);
					System.out.println(Thread.currentThread().getName() + " : "+ local.get());

				}
			}, "Thread-" + j);
		}

		for (Thread thread : threads) {
			thread.start();
		}
	}
}
運行後結果:

Thread-0 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5 事務

咱們看到,每一個線程累加後的結果都是5,各個線程處理本身的本地變量值,線程之間互不影響。

咱們再來看一個例子:

public class ThreadLocalTest {
	private static Index num = new Index();
        //建立一個Index類型的本地變量 
	private static ThreadLocal<Index> local = new ThreadLocal<Index>() {
		@Override
		protected Index initialValue() {
			return num;
		}
	};

	public static void main(String[] args) throws InterruptedException {
		Thread[] threads = new Thread[5];
		for (int j = 0; j < 5; j++) {
			threads[j] = new Thread(new Runnable() {
				@Override
				public void run() {
                                        //取出當前線程的本地變量,並累加1000次
					Index index = local.get();
					for (int i = 0; i < 1000; i++) {                                          
						index.increase();
					}
					System.out.println(Thread.currentThread().getName() + " : "+ index.num);

				}
			}, "Thread-" + j);
		}
		for (Thread thread : threads) {
			thread.start();
		}
	}

	static class Index {
		int num;

		public void increase() {
			num++;
		}
	}
}
執行後咱們發現結果以下(每次運行還都不同):


Thread-0 : 1390
Thread-2 : 2390
Thread-4 : 4390
Thread-3 : 3491
Thread-1 : 1390

此次爲何線程本地變量又失效了呢?你們能夠仔細觀察上面代碼本身先找一下緣由。

-----------------------------------------------低調的分割線-------------------------------------------

讓咱們再來回味一下 「ThreadLocal能夠給一個初始值,而每一個線程都會得到這個初始化值的一個副本」 這句話。「初始值的副本。。。」,貌似想起點什麼。咱們再來看一下上面代碼中定義ThreadLocal的地方


private static Index num = new Index();
	private static ThreadLocal<Index> local = new ThreadLocal<Index>() {
		@Override
		protected Index initialValue() {
			return num;       // 注意這裏,返回的是已經定義好的對象num,而不是new Index()
		}
	};


 上面代碼中,咱們經過覆蓋initialValue函數來給咱們的ThreadLocal提供初始值,每一個線程都會獲取這個初始值的一個副本。而如今咱們的初始值是一個定義好的一個對象,num是這個對象的引用.換句話說咱們的初始值是一個引用。引用的副本和引用指向的不就是同一個對象嗎?

若是咱們想給每個線程都保存一個Index對象應該怎麼辦呢?那就是建立對象的副本而不是對象引用的副本:

private static ThreadLocal<Index> local = new ThreadLocal<Index>() {
		@Override
		protected Index initialValue() {
			return new Index(); //注意這裏
		}
	};

對象的拷貝圖示:

 

如今咱們應該能明白ThreadLocal本地變量的含義了吧。接下來咱們就來看看ThreadLocal的源碼,從內部來揭示它的神祕面紗。

ThreadLocal有一個內部類ThreadLocalMap,這個類的實現佔了整個ThreadLocal類源碼的一多半。這個ThreadLocalMap的做用很是關鍵,它就是線程真正保存線程本身本地變量的容器。每個線程都有本身的單獨的一個ThreadLocalMap實例,其全部的本地變量都會保存到這一個map中。如今就讓咱們從ThreadLocal的get和set這兩個最經常使用的方法開始分析:

public T get() {
        //獲取當前執行線程
        Thread t = Thread.currentThread();
        //取得當前線程的ThreadLocalMap實例
        ThreadLocalMap map = getMap(t);
        //若是map不爲空,說明該線程已經有了一個ThreadLocalMap實例
        if (map != null) {
            //map中保存線程的全部的線程本地變量,咱們要去查找當前線程本地變量
            ThreadLocalMap.Entry e = map.getEntry(this);
            //若是當前線程本地變量存在這個map中,則返回其對應的值
            if (e != null)
                return (T)e.value;
        }
        //若是map不存在或者map中不存在當前線程本地變量,返回初始值
        return setInitialValue();
    }

強調一下:Thread對象都有一個ThreadLocalMap類型的屬性threadLocals,這個屬性是專門用於保存本身全部的線程本地變量的。這個屬性在線程對象初始化的時候爲null。因此對一個線程對象第一次使用線程本地變量的時候,須要對這個threadLocals屬性進行初始化操做。注意要區別 「線程第一次使用本地線程變量」和「第一次使用某一個線程本地線程變量」。

getMap方法:

//直接返回線程對象的threadLocals屬性
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }


setInitialValue方法:(看完後再回顧一下以前的那個例子)

private T setInitialValue() {
        //獲取初始化值,initialValue 就是咱們以前覆蓋的方法
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //若是map不爲空,將初始化值放入到當前線程的ThreadLocalMap對象中
        if (map != null)
            map.set(this, value);
        else
            //當前線程第一次使用本地線程變量,須要對map進行初始化工做
            createMap(t, value);
        //返回初始化值
        return value;
    }


咱們再來看一下set方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);

        if (map != null)
            map.set(this, value);
        //說明線程第一次使用線程本地變量(注意這裏的第一次含義)
        else
            createMap(t, value);
    }


ThradLocal還有一個remove方法,讓咱們來分析一下:

public void remove() {
         //獲取當前線程的ThreadLocalMap對象
         ThreadLocalMap m = getMap(Thread.currentThread());
         //若是map不爲空,則刪除該本地變量的值
         if (m != null)
             m.remove(this);
     }

到這裏你們應該對ThreadLocal變量比較清晰了,至於ThradLocalMap的實現細節這裏就不在說了。你們有興趣能夠本身去看ThreadLocal的源碼。

相關文章
相關標籤/搜索