Java併發——ThreadLocal分析

簡述

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();
            }
        }
    }
     
}
複製代碼

運行結果:ide

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確實是能夠達到線程隔離機制,保證了變量的安全性this

ThreadLocal實現原理

ThreadLocal是爲每個線程建立一個單獨的變量副本,因此每一個線程均可以獨立地改變本身所擁有的變量副本,而不會影響其餘線程所對應的副本。從其幾個方法入手
spa

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直接存儲便可code

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()方法orm

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值,定位哈希表的位置:
一、若此位置是空的,就建立一個Entry對象放在此位置上,調用cleanSomeSlots()方法清除key爲null的舊entry,若沒有要清除的舊entry則判斷是否須要擴容
二、若此位置已經有Entry對象了,若是這個Entry對象的key正好是所要設置的key或key爲null,則替換value值
三、若此位置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才能 獲得釋放

那麼如何有效的避免呢?

在上述中咱們能夠看到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

相關文章
相關標籤/搜索