深入理解Java ThreadLocal

jopen 11年前發布 | 15K 次閱讀 Java開發 ThreadLocal

學習一個東西首先要知道為什么要引入它,就是我們能用它來干什么。所以我們先來看看ThreadLocal對我們到底有什么用,然后再來看看它的實現原理。

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

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


1、每個線程都有自己的局部變量

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

2、獨立于變量的初始化副本

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

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

    ThreadLocal 不是用于解決共享變量的問題的,不是為了協調線程同步而存在,而是為了方便每個線程處理自己的狀態而引入的一個機制,理解這點對正確使用ThreadLocal至關重要。

我們先看一個簡單的例子:

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

}</pre>運行后結果:

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++;
    }
}

}</pre>執行后我們發現結果如下(每次運行還都不一樣):


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);
}</pre> <p><br />

</p>

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

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

到這里大家應該對ThreadLocal變量比較清晰了,至于ThradLocalMap的實現細節這里就不在說了。大家有興趣可以自己去看ThreadLocal的源碼。


來自:http://my.oschina.net/clopopo/blog/149368

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!