淺談java.lang.ThreadLocal類

相信讀者在網上也看了很多關於ThreadLocal的資料,很多博客都這樣說:ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路;ThreadLocal的目的是爲了解決多線程訪問資源時的共享問題。如果你也這樣認爲的,那現在給你10秒鐘,清空之前對ThreadLocal的錯誤的認知!

看看JDK中的源碼是怎麼寫的:

This class provides thread-local variables.  These variables differ from
their normal counterparts in that each thread that accesses one (via its
get or set method) has its own, independently initialized
copy of the variable.  ThreadLocal instances are typically private
static fields in classes that wish to associate state with a thread (e.g.,
a user ID or Transaction ID).

翻譯過來大概是這樣的(英文不好,如有更好的翻譯,請留言說明):

ThreadLocal類用來提供線程內部的局部變量。這種變量在多線程環境下訪問(
通過get或set方法訪問)時能保證各個線程裏的變量相對獨立於其他線程內的變量。
ThreadLocal實例通常來說都是private static類型的,用於關聯線程和線程的上下文。

可以總結爲一句話:ThreadLocal的作用是提供線程內的局部變量,這種變量在線程的生命週期內起作用,減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度。

舉個例子,我出門需要先坐公交再做地鐵,這裏的坐公交和坐地鐵就好比是同一個線程內的兩個函數,我就是一個線程,我要完成這兩個函數都需要同一個東西:公交卡(北京公交和地鐵都使用公交卡),那麼我爲了不向這兩個函數都傳遞公交卡這個變量(相當於不是一直帶着公交卡上路),我可以這麼做:將公交卡事先交給一個機構,當我需要刷卡的時候再向這個機構要公交卡(當然每次拿的都是同一張公交卡)。這樣就能達到只要是我(同一個線程)需要公交卡,何時何地都能向這個機構要的目的。

有人要說了:你可以將公交卡設置爲全局變量啊,這樣不是也能何時何地都能取公交卡嗎?但是如果有很多個人(很多個線程)呢?大家可不能都使用同一張公交卡吧(我們假設公交卡是實名認證的),這樣不就亂套了嘛。現在明白了吧?這就是ThreadLocal設計的初衷:提供線程內部的局部變量,在本線程內隨時隨地可取,隔離其他線程

1、ThreadLocal基本操作

  • 構造函數

ThreadLocal的構造函數是這樣的:  

/**
* Creates a thread local variable.
*/
public ThreadLocal() {
}

內部啥也沒做。

  • initialValue函數

initialValue函數用來設置ThreadLocal的初始值,如下:

protected T initialValue() {
    return null;
}

該函數在調用get函數的時候會第一次調用,但是如果一開始就調用了set函數,則該函數不會被調用。通常該函數只會被調用一次,除非手動調用了remove函數之後又調用get函數,這種情況下,get函數中還是會調用initialValue函數。該函數是protected類型的,很顯然是建議在子類重載該函數的,所以通常該函數都會以匿名內部類的形式被重載,以指定初始值,比如:

package test;

public class ThreadLocalDemo {
     
    private final static ThreadLocal<Integer> seqNum  = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    
}
  • get函數

該函數用來獲取與當前線程關聯的ThreadLocal的值,如下:

public T get()

如果當前線程沒有該ThreadLocal的值,則調用initialValue函數獲取初始值返回。

  • set函數

set函數用來設置當前線程的該ThreadLocal的值,如下:

public void set(T value)

設置當前線程的ThreadLocal的值爲value。

  • remove函數

remove函數用來將當前線程的ThreadLocal綁定的值刪除,如下:

public void remove()

在某些情況下需要手動調用該函數,防止內存泄露。

2、代碼演示

學習了最基本的操作之後,我們用一段代碼來演示ThreadLocal的用法,該例子實現下面這個場景:

有5個線程,這5個線程都有一個序列值seqNum,初始值爲0,線程運行時序列值seqNum遞增。

代碼實現:

package test;

public class ThreadLocalDemo implements Runnable {
    
    public static void main(String[] args) {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        
        for (int i = 0; i < 5; i ++) {
            Thread t = new Thread(threadLocalDemo, String.valueOf(i));
            t.start();
        }
    }
    
    private final static ThreadLocal<Integer> seqNum  = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    
    public int getNextNum() {
        seqNum.set(seqNum.get() + 1);
        return seqNum.get();
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i ++) {
            System.out.println("thread[" + Thread.currentThread().getName() 
+ "] --> [" + getNextNum() + "]");  
        }
    }
    
}

執行結果爲:

thread[0] --> [1]
thread[0] --> [2]
thread[0] --> [3]
thread[4] --> [1]
thread[1] --> [1]
thread[1] --> [2]
thread[1] --> [3]
thread[2] --> [1]
thread[3] --> [1]
thread[3] --> [2]
thread[2] --> [2]
thread[4] --> [2]
thread[4] --> [3]
thread[2] --> [3]
thread[3] --> [3]

可以看到,各個線程的seqNum值是相互獨立的,本線程的遞增操作不會影響到其他線程的值,真正達到了線程內部隔離的效果。

3、如何實現

看了基本介紹,也看了最簡單的效果演示之後,我們更應該好好研究下ThreadLocal內部的實現原理。如果給你設計,你會怎麼設計?相信大部分人會有這樣的想法:

每個ThreadLocal類創建一個Map,然後用線程的ID作爲Map的key,實例對象作爲Map的value,這樣就能達到各個線程的值隔離的效果。

沒錯,這是最簡單的設計方案,JDK最早期的ThreadLocal就是這樣設計的。JDK1.3(不確定是否是1.3)之後ThreadLocal的設計換了一種方式。

我們先看看JDK7的ThreadLocal的get方法的源碼:

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

其中getMap的源碼:

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

setInitialValue函數的源碼:

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

createMap函數的源碼:

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

簡單解析一下,get方法的流程是這樣的:

  1. 首先獲取當前線程;
  2. 根據當前線程獲取一個Map;
  3. 如果獲取的Map不爲空,則在Map中以ThreadLocal的引用作爲key來在Map中獲取對應的value e,否則轉到5;
  4. 如果e不爲null,則返回e.value,否則轉到5;
  5. Map爲空或者e爲空,則通過initialValue函數獲取初始值value,然後用ThreadLocal的引用和value作爲firstKey和firstValue創建一個新的Map。

然後需要注意的是Thread類中包含一個成員變量:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

所以,可以總結一下ThreadLocal的設計思路: 
每個Thread維護一個ThreadLocalMap映射表,這個映射表的key是ThreadLocal實例本身,value是真正需要存儲的Object。 
這個方案剛好與我們開始說的簡單的設計方案相反。查閱了一下資料,這樣設計的主要有以下幾點優勢:

  • 這樣設計之後每個Map的Entry數量變小了:之前是Thread的數量,現在是ThreadLocal的數量,能提高性能,據說性能的提升不是一點兩點(沒有親測)
  • 當Thread銷燬之後對應的ThreadLocalMap也就隨之銷燬了,能減少內存使用量。

4、使用場景

ThreadLocal使用場合主要解決多線程中數據因併發產生不一致問題。ThreadLocal爲每個線程的中併發訪問的數據提供一個副本,通過訪問副本來運行業務,這樣的結果是耗費了內存,大大減少了線程同步所帶來性能消耗,也減少了線程併發控制的複雜度。

ThreadLocal不能使用原子類型,只能使用Object類型。ThreadLocal的使用比synchronized要簡單得多。

ThreadLocal和Synchonized都用於解決多線程併發訪問。但是ThreadLocal與synchronized有本質的區別。

1. synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。

2. ThreadLocal爲每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的並不是同一個對象,這樣就隔離了多個線程對數據的數據共享。而Synchronized卻正好相反,它用於在多個線程間通信時能夠獲得數據共享。

Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離

當然ThreadLocal並不能替代synchronized,它們處理不同的問題域。Synchronized用於實現同步機制,比ThreadLocal更加複雜。總結一句話就是一個是鎖機制進行時間換空間,一個是存儲拷貝進行空間換時間。

參考

http://blog.csdn.net/winwill2012/article/details/71625570

http://blog.csdn.net/lufeng20/article/details/24314381

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章