Java併發——ThreadLocal分析

午夜12點發表於2018-09-07

簡述

jdk原始碼註解中有這樣一段描述:

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).

這個類提供執行緒區域性變數。這些變數與其正常的對應方式不同,因為訪問一個的每個執行緒(通過其get或set方法)都有自己獨立初始化的變數副本。 ThreadLocal例項通常是希望將狀態與執行緒關聯的類中的私有靜態欄位(例如,使用者ID或事務ID)

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

ThreadLocal使用示例


public class SeqCount {
     
    private static ThreadLocal seqCount = new ThreadLocal(){
        // 實現initialValue()
        public Integer initialValue() {
            return 0;
        }
    };
     
    public int nextSeq(){
        seqCount.set(seqCount.get() + 1);
      
        return seqCount.get();
    }
    
    public void remove() {
        seqCount.remove();
    }
     
    public static void main(String[] args){
        SeqCount seqCount = new SeqCount();
     
        SeqThread thread1 = new SeqThread(seqCount);
        SeqThread thread2 = new SeqThread(seqCount);
        SeqThread thread3 = new SeqThread(seqCount);
        SeqThread thread4 = new SeqThread(seqCount);
     
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
        
    private static class SeqThread extends Thread{
        private SeqCount seqCount;
         
        SeqThread(SeqCount seqCount){
            this.seqCount = seqCount;
        }
         
        public void run() {
            try {
                for(int i = 0 ; i < 3 ; i++){
                    System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
                }
            } finally {
                seqCount.remove();
            }
        }
    }
     
}
複製程式碼

執行結果:


Thread-0 seqCount :1
Thread-0 seqCount :2
Thread-0 seqCount :3
Thread-1 seqCount :1
Thread-1 seqCount :2
Thread-1 seqCount :3
Thread-3 seqCount :1
Thread-3 seqCount :2
Thread-3 seqCount :3
Thread-2 seqCount :1
Thread-2 seqCount :2
Thread-2 seqCount :3
複製程式碼

從結果可以得知,ThreadLocal確實是可以達到執行緒隔離機制,保證了變數的安全性

ThreadLocal實現原理

ThreadLocal是為每一個執行緒建立一個單獨的變數副本,所以每個執行緒都可以獨立地改變自己所擁有的變數副本,而不會影響其他執行緒所對應的副本。從其幾個方法入手

set方法


    public void set(T value) {
        // 獲取當前執行緒
        Thread t = Thread.currentThread();
        // 通過當前執行緒例項獲取ThreadLocalMap物件
        ThreadLocalMap map = getMap(t);
        // 若map不為null,則以當前threadLocal為鍵,value為值存放
        if (map != null)
            map.set(this, value);
        // 若map為null,則建立ThreadLocalMap,以當前threadLocal為鍵,value為值
        else
            createMap(t, value);
    }
複製程式碼

獲取當前執行緒例項,呼叫getMap()獲取此執行緒的ThreadLocalMap


    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
複製程式碼

然後判斷map是否為null,若為null則還需建立threadLocalMap,以當前threadLocal為鍵,value為值存放在threadLocalMap中,若不為null直接儲存即可

get方法


    public T get() {
        // 獲取當前執行緒
        Thread t = Thread.currentThread();
        // 獲取執行緒關聯的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 若map不為null,從map中獲取以當前threadLocal例項為key的資料
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 若map為null或者entry為null,則呼叫此方法初始化
        return setInitialValue();
    }
複製程式碼

get方法獲取當前執行緒關聯的ThreadLocalMap。若map不為null,以threadLocal例項為key獲取資料;若map為null或entry為null呼叫setInitialValue()方法


    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;
    }
複製程式碼

與set方法差不多,但多了initialValue()方法,此方法需要子類重寫


    protected T initialValue() {
        return null;
    }
複製程式碼

remove方法


    public void remove() {
         // 根據當前執行緒獲取其所關聯的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         // 若map不為null,刪除以當前threadLocal為key的資料
         if (m != null)
             m.remove(this);
     }
複製程式碼

ThreadLocalMap

從ThreadLocal那幾個核心方法來看,其實現都基於內部類ThreadLocalMap

ThreadLocalMap屬性


    // 初始化容量
    private static final int INITIAL_CAPACITY = 16;
    // 雜湊表     
    private Entry[] table;
    // 元素個數     
    private int size = 0;
    // 擴容閾值(threshold = 底層雜湊表table的長度 len * 2 / 3)
    private int threshold;
複製程式碼

內部類entry


    static class Entry extends WeakReference> {
        /** The value associated with this ThreadLocal. */
        Object value;
         
        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }
複製程式碼

從原始碼中可以得知Entry的key是Threadlocal,並且Entry繼承WeakReference弱引用。注意Entry中並沒有next屬性,相對於HashMap採用鏈地址法處理衝突,ThreadLocalMap採用開放定址法

set方法


    private void set(ThreadLocal key, Object value) {
               
        Entry[] tab = table;
        int len = tab.length;
        // 根據ThreadLocal的hashcode值,尋找對應Entry在陣列中的位置
        int i = key.threadLocalHashCode & (len-1);
             
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
             
            ThreadLocal k = e.get();
            // 若找到對應key,替換舊值返回 
            if (k == key) {
                e.value = value;
                return;
            }
            // 若key == null,因為e!=null肯定存在entry
            // 說明之前的ThreadLocal物件已經被回收
            if (k == null) {
                // 替換舊entry
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 建立新entry  
        tab[i] = new Entry(key, value);
        // 元素個數+1
        int sz = ++size;
        // cleanSomeSlots 清除舊Entry(key == null)
        // 如果沒有要清除的資料,元素個數仍然大於閾值則擴容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
複製程式碼

每個ThreadLocal物件都有一個hash值threadLocalHashCode,每初始化一個ThreadLocal物件,hash值就增加一個固定的大小0x61c88647。在插入過程中先根據threadlocal物件的hash值,定位雜湊表的位置:
1、若此位置是空的,就建立一個Entry物件放在此位置上,呼叫cleanSomeSlots()方法清除key為null的舊entry,若沒有要清除的舊entry則判斷是否需要擴容
2、若此位置已經有Entry物件了,如果這個Entry物件的key正好是所要設定的key或key為null,則替換value值
3、若此位置Entry物件的key不符合條件,尋找雜湊表此位置+1(若到達雜湊表尾則從頭開始)

我們可以發現ThreadLocalMap採用了開放定址法來解決衝突,一旦發生了衝突,就去尋找下一個空的雜湊地址,而HashMap採用鏈地址法解決衝突在原位置利用連結串列處理

getEntry方法


    private Entry getEntry(ThreadLocal key) {
        // 定位
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        // 若此位置不為空且與entry的key返回entry物件
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
複製程式碼

理解了set,getEntry很好理解。先根據threadlocal物件的hash值,定位雜湊表的位置。若此位置entry的key和查詢的key相同的話就直接返回這個entry,若不符合呼叫getEntryAfterMiss()繼續向後找,getEntryAfterMiss方法如下:


    private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
        
        while (e != null) {
            ThreadLocal k = e.get();
            // 找到和所需key相同的entry則返回
            if (k == key)
                return e;
            // 處理key為null的entry    
            if (k == null)
                expungeStaleEntry(i);
            else
                // 繼續找下一個
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }
複製程式碼

remove方法


    private void remove(ThreadLocal key) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            // 若找到所需key 
            if (e.get() == key) {
                // 將entry的key置為null
                e.clear();
                // 將entry的value置為null同時entry置空
                expungeStaleEntry(i);
                return;
            }
        }
    }
複製程式碼

定位在雜湊表的位置,找到相同key的entry,呼叫clear方法將key置為null,呼叫expungeStaleEntry方法刪除對應位置的過期實體,並刪除此位置後key = null的實體


        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            // 將此位置的entry物件置空以及value置空
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            // 元素個數-1
            size--;
             
            // Rehash until we encounter null
            Entry e;
            int i;
            // 清除此位置後key為null的entry物件以及rehash位置不同的entry直至有位置為空為止
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                         
                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
複製程式碼

記憶體洩漏

先附上四種引用與gc關係

引用型別 回收機制 用途 生存時間
強引用 從不回收 物件狀態 JVM停止執行時
軟引用 記憶體不足時回收 物件快取 記憶體不足時終止
弱引用 物件不被引用時回收 物件快取 GC後終止
虛引用 物件不被引用時回收 跟蹤物件的垃圾回收 垃圾回收後終止
先看下面程式碼

    Son son = new Son(); 
    Parent parent = new Parent(son); 
複製程式碼
當我們把son置空,由於parent持有son的引用且parent是強引用,所以gc並不回收son所分配的記憶體空間,這就導致了記憶體洩露

如果是弱引用那麼上述例子,GC就會回收son所分配的記憶體空間。而ThreadLocalMap採用ThreadLocal弱引用作為key,雖然ThreadLocal是弱引用GC會回收這部分空間即key被回收,但是value卻存在一條從Current Thread過來的強引用鏈。因此只有當Current Thread銷燬時,value才能 得到釋放

Java併發——ThreadLocal分析

那麼如何有效的避免呢?

在上述中我們可以看到ThreadLocalMap中的set/getEntry方法中,會對key為null(即ThreadLocal為null)進行判斷,如果為null的話,那麼是會對value置為null的。當然也可以通過呼叫ThreadLocal的remove方法進行釋放。

總結

ThreadLocal不是用來解決共享物件的多執行緒訪問問題,而是為了方便每個執行緒處理自己的狀態而引入的一個機制。它為每一個執行緒都提供一份變數的副本,從而實現同時訪問而互不影響。另外ThreadLocal可能存在記憶體洩漏問題,使用完ThreadLocal之後,最好呼叫remove方法

感謝

www.jianshu.com/p/377bb8408…
www.jianshu.com/p/ee8c9dccc…
cmsblogs.com/?p=2442

相關文章