歡迎光臨
每天分享高質量文章

【死磕Java併發】—–深入分析ThreadLocal

你不一定要點藍字關註我的

ThreadLoacal是什麼?

ThreadLocal是啥?以前面試別人時就喜歡問這個,有些夥伴喜歡把它和執行緒同步機制混為一談,事實上ThreadLocal與執行緒同步無關。ThreadLocal雖然提供了一種解決多執行緒環境下成員變數的問題,但是它並不是解決多執行緒共享變數的問題。那麼ThreadLocal到底是什麼呢?

API是這樣介紹它的:This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code 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).

該類提供了執行緒區域性 (thread-local) 變數。這些變數不同於它們的普通對應物,因為訪問某個變數(透過其 getset 方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本。 ThreadLocal實體通常是類中的 private static 欄位,它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。

所以ThreadLocal與執行緒同步機制不同,執行緒同步機制是多個執行緒共享同一個變數,而ThreadLocal是為每一個執行緒建立一個單獨的變數副本,故而每個執行緒都可以獨立地改變自己所擁有的變數副本,而不會影響其他執行緒所對應的副本。可以說ThreadLocal為多執行緒環境下變數問題提供了另外一種解決思路。

ThreadLocal定義了四個方法:

  • get():傳回此執行緒區域性變數的當前執行緒副本中的值。

  • initialValue():傳回此執行緒區域性變數的當前執行緒的“初始值”。

  • remove():移除此執行緒區域性變數當前執行緒的值。

  • set(T value):將此執行緒區域性變數的當前執行緒副本中的值設定為指定值。

除了這四個方法,ThreadLocal內部還有一個靜態內部類ThreadLocalMap,該內部類才是實現執行緒隔離機制的關鍵,get()、set()、remove()都是基於該內部類操作。ThreadLocalMap提供了一種用鍵值對方式儲存每一個執行緒的變數副本的方法,key為當前ThreadLocal物件,value則是對應執行緒的變數副本。

對於ThreadLocal需要註意的有兩點:

  1. ThreadLocal實體本身是不儲存值,它只是提供了一個在當前執行緒中找到副本值得key。

  2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小夥伴會弄錯他們的關係。

下圖是Thread、ThreadLocal、ThreadLocalMap的關係(http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/)

ThreadLocal使用示例

示例如下:

  1. public class SeqCount {

  2.    private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){

  3.        // 實現initialValue()

  4.        public Integer initialValue() {

  5.            return 0;

  6.        }

  7.    };

  8.    public int nextSeq(){

  9.        seqCount.set(seqCount.get() + 1);

  10.        return seqCount.get();

  11.    }

  12.    public static void main(String[] args){

  13.        SeqCount seqCount = new SeqCount();

  14.        SeqThread thread1 = new SeqThread(seqCount);

  15.        SeqThread thread2 = new SeqThread(seqCount);

  16.        SeqThread thread3 = new SeqThread(seqCount);

  17.        SeqThread thread4 = new SeqThread(seqCount);

  18.        thread1.start();

  19.        thread2.start();

  20.        thread3.start();

  21.        thread4.start();

  22.    }

  23.    private static class SeqThread extends Thread{

  24.        private SeqCount seqCount;

  25.        SeqThread(SeqCount seqCount){

  26.            this.seqCount = seqCount;

  27.        }

  28.        public void run() {

  29.            for(int i = 0 ; i < 3 ; i++){

  30.                System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());

  31.            }

  32.        }

  33.    }

  34. }

執行結果:

從執行結果可以看出,ThreadLocal確實是可以達到執行緒隔離機制,確保變數的安全性。這裡我們想一個問題,在上面的程式碼中ThreadLocal的initialValue()方法傳回的是0,加入該方法傳回得是一個物件呢,會產生什麼後果呢?例如:

  1.    A a = new A();

  2.    private static ThreadLocal<A> seqCount = new ThreadLocal<A>(){

  3.        // 實現initialValue()

  4.        public A initialValue() {

  5.            return a;

  6.        }

  7.    };

  8.    class A{

  9.        // ....

  10.    }

具體過程請參考:對ThreadLocal實現原理的一點思考

ThreadLocal原始碼解析

ThreadLocal雖然解決了這個多執行緒變數的複雜問題,但是它的原始碼實現卻是比較簡單的。ThreadLocalMap是實現ThreadLocal的關鍵,我們先從它入手。

ThreadLocalMap

ThreadLocalMap其內部利用Entry來實現key-value的儲存,如下:

  1.       static class Entry extends WeakReference<ThreadLocal>> {

  2.            /** The value associated with this ThreadLocal. */

  3.            Object value;

  4.            Entry(ThreadLocal> k, Object v) {

  5.                super(k);

  6.                value = v;

  7.            }

  8.        }

從上面程式碼中可以看出Entry的key就是ThreadLocal,而value就是值。同時,Entry也繼承WeakReference,所以說Entry所對應key(ThreadLocal實體)的取用為一個弱取用(關於弱取用這裡就不多說了,感興趣的可以關註這篇部落格:Java 理論與實踐: 用弱取用堵住記憶體洩漏)

ThreadLocalMap的原始碼稍微多了點,我們就看兩個最核心的方法getEntry()、set(ThreadLocal key, Object value)方法。

set(ThreadLocal key, Object value)

  1.    private void set(ThreadLocal> key, Object value) {

  2.        ThreadLocal.ThreadLocalMap.Entry[] tab = table;

  3.        int len = tab.length;

  4.        // 根據 ThreadLocal 的雜湊值,查詢對應元素在陣列中的位置

  5.        int i = key.threadLocalHashCode & (len-1);

  6.        // 採用“線性探測法”,尋找合適位置

  7.        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];

  8.            e != null;

  9.            e = tab[i = nextIndex(i, len)]) {

  10.            ThreadLocal> k = e.get();

  11.            // key 存在,直接改寫

  12.            if (k == key) {

  13.                e.value = value;

  14.                return;

  15.            }

  16.            // key == null,但是存在值(因為此處的e != null),說明之前的ThreadLocal物件已經被回收了

  17.            if (k == null) {

  18.                // 用新元素替換陳舊的元素

  19.                replaceStaleEntry(key, value, i);

  20.                return;

  21.            }

  22.        }

  23.        // ThreadLocal對應的key實體不存在也沒有陳舊元素,new 一個

  24.        tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

  25.        int sz = ++size;

  26.        // cleanSomeSlots 清楚陳舊的Entry(key == null)

  27.        // 如果沒有清理陳舊的 Entry 並且陣列中的元素大於了閾值,則進行 rehash

  28.        if (!cleanSomeSlots(i, sz) && sz >= threshold)

  29.            rehash();

  30.    }

這個set()操作和我們在集合瞭解的put()方式有點兒不一樣,雖然他們都是key-value結構,不同在於他們解決雜湊衝突的方式不同。集合Map的put()採用的是拉鏈法,而ThreadLocalMap的set()則是採用開放定址法(具體請參考雜湊衝突處理系列部落格)。掌握了開放地址法該方法就一目瞭然了。

set()操作除了儲存元素外,還有一個很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key == null 的實體,防止記憶體洩漏。在set()方法中還有一個變數很重要:threadLocalHashCode,定義如下:

  1. private final int threadLocalHashCode = nextHashCode();

從名字上面我們可以看出threadLocalHashCode應該是ThreadLocal的雜湊值,定義為final,表示ThreadLocal一旦建立其雜湊值就已經確定了,生成過程則是呼叫nextHashCode():

  1.    private static AtomicInteger nextHashCode = new AtomicInteger();

  2.    private static final int HASH_INCREMENT = 0x61c88647;

  3.    private static int nextHashCode() {

  4.        return nextHashCode.getAndAdd(HASH_INCREMENT);

  5.    }

nextHashCode表示分配下一個ThreadLocal實體的threadLocalHashCode的值,HASH_INCREMENT則表示分配兩個ThradLocal實體的threadLocalHashCode的增量,從nextHashCode就可以看出他們的定義。

getEntry()

  1.        private Entry getEntry(ThreadLocal> key) {

  2.            int i = key.threadLocalHashCode & (table.length - 1);

  3.            Entry e = table[i];

  4.            if (e != null && e.get() == key)

  5.                return e;

  6.            else

  7.                return getEntryAfterMiss(key, i, e);

  8.        }

由於採用了開放定址法,所以當前key的雜湊值和元素在陣列的索引並不是完全對應的,首先取一個探測數(key的雜湊值),如果所對應的key就是我們所要找的元素,則傳回,否則呼叫getEntryAfterMiss(),如下:

  1.        private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {

  2.            Entry[] tab = table;

  3.            int len = tab.length;

  4.            while (e != null) {

  5.                ThreadLocal> k = e.get();

  6.                if (k == key)

  7.                    return e;

  8.                if (k == null)

  9.                    expungeStaleEntry(i);

  10.                else

  11.                    i = nextIndex(i, len);

  12.                e = tab[i];

  13.            }

  14.            return null;

  15.        }

這裡有一個重要的地方,當key == null時,呼叫了expungeStaleEntry()方法,該方法用於處理key == null,有利於GC回收,能夠有效地避免記憶體洩漏。

get()

傳回當前執行緒所對應的執行緒變數

  1.    public T get() {

  2.        // 獲取當前執行緒

  3.        Thread t = Thread.currentThread();

  4.        // 獲取當前執行緒的成員變數 threadLocal

  5.        ThreadLocalMap map = getMap(t);

  6.        if (map != null) {

  7.            // 從當前執行緒的ThreadLocalMap獲取相對應的Entry

  8.            ThreadLocalMap.Entry e = map.getEntry(this);

  9.            if (e != null) {

  10.                @SuppressWarnings("unchecked")

  11.                // 獲取標的值        

  12.                T result = (T)e.value;

  13.                return result;

  14.            }

  15.        }

  16.        return setInitialValue();

  17.    }

首先透過當前執行緒獲取所對應的成員變數ThreadLocalMap,然後透過ThreadLocalMap獲取當前ThreadLocal的Entry,最後透過所獲取的Entry獲取標的值result。

getMap()方法可以獲取當前執行緒所對應的ThreadLocalMap,如下:

  1.    ThreadLocalMap getMap(Thread t) {

  2.        return t.threadLocals;

  3.    }

set(T value)

設定當前執行緒的執行緒區域性變數的值。

  1.    public void set(T value) {

  2.        Thread t = Thread.currentThread();

  3.        ThreadLocalMap map = getMap(t);

  4.        if (map != null)

  5.            map.set(this, value);

  6.        else

  7.            createMap(t, value);

  8.    }

獲取當前執行緒所對應的ThreadLocalMap,如果不為空,則呼叫ThreadLocalMap的set()方法,key就是當前ThreadLocal,如果不存在,則呼叫createMap()方法新建一個,如下:

  1.    void createMap(Thread t, T firstValue) {

  2.        t.threadLocals = new ThreadLocalMap(this, firstValue);

  3.    }

initialValue()

傳回該執行緒區域性變數的初始值。

  1.    protected T initialValue() {

  2.        return null;

  3.    }

該方法定義為protected級別且傳回為null,很明顯是要子類實現它的,所以我們在使用ThreadLocal的時候一般都應該改寫該方法。該方法不能顯示呼叫,只有在第一次呼叫get()或者set()方法時才會被執行,並且僅執行1次。

remove()

將當前執行緒區域性變數的值刪除。

  1.    public void remove() {

  2.        ThreadLocalMap m = getMap(Thread.currentThread());

  3.        if (m != null)

  4.            m.remove(this);

  5.    }

該方法的目的是減少記憶體的佔用。當然,我們不需要顯示呼叫該方法,因為一個執行緒結束後,它所對應的區域性變數就會被垃圾回收。

ThreadLocal為什麼會記憶體洩漏

前面提到每個Thread都有一個ThreadLocal.ThreadLocalMap的map,該map的key為ThreadLocal實體,它為一個弱取用,我們知道弱取用有利於GC回收。當ThreadLocal的key == null時,GC就會回收這部分空間,但是value卻不一定能夠被回收,因為他還與Current Thread存在一個強取用關係,如下(圖片來自http://www.jianshu.com/p/ee8c9dccc953):

由於存在這個強取用關係,會導致value無法回收。如果這個執行緒物件不會銷毀那麼這個強取用關係則會一直存在,就會出現記憶體洩漏情況。所以說只要這個執行緒物件能夠及時被GC回收,就不會出現記憶體洩漏。如果碰到執行緒池,那就更坑了。

那麼要怎麼避免這個問題呢?

在前面提過,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情況,會對value設定為null。當然我們也可以顯示呼叫ThreadLocal的remove()方法進行處理。

下麵再對ThreadLocal進行簡單的總結:

  • ThreadLocal 不是用於解決共享變數的問題的,也不是為了協調執行緒同步而存在,而是為了方便每個執行緒處理自己的狀態而引入的一個機制。這點至關重要。

  • 每個Thread內部都有一個ThreadLocal.ThreadLocalMap型別的成員變數,該成員變數用來儲存實際的ThreadLocal變數副本。

  • ThreadLocal並不是為執行緒儲存物件的副本,它僅僅只起到一個索引的作用。它的主要木得視為每一個執行緒隔離一個類的實體,這個實體的作用範圍僅限於執行緒內部。

END

贊(0)

分享創造快樂

© 2024 知識星球   網站地圖