Java併發之ThreadLocal深度解析

ThreadLocal是什麼

     首先說明,ThreadLocal與線程同步無關。ThreadLocal雖然提供了一種解決多線程環境下成員變量的問題,但是它並不是解決多線程共享變量的問題。

     ThreadLocal類提供了一種線程局部變量(ThreadLocal),即每一個線程都會保存一份變量副本,每個線程都可以獨立地修改自己的變量副本,而不會影響到其他線程,是一種線程隔離的思想。

實現原理

ThreadLocal提供四個方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

     get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本,set()用來設置當前線程中變量的副本,remove()用來移除當前線程中變量的副本,initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法。這四種方法都是基於ThreadLocalMap的。

ThreadLocalMap

     ThreadLocal內部有一個靜態內部類ThreadLocalMap,該內部類是實現線程隔離機制的關鍵。ThreadLocalMap提供了一種用鍵值對方式存儲每一個線程的變量副本的方法,key爲當前ThreadLocal對象,value則是對應線程的變量副本。該Map默認的大小是16,即能存儲16個鍵值對,超過後會擴容。

具體源碼如下:

Entry類

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

 static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

     從上面代碼中可以看出Entry的key就是ThreadLocal,而value就是值。同時,Entry也繼承WeakReference,所以說Entry所對應key(ThreadLocal實例)的引用爲一個弱引用。

set方法

 private void set(ThreadLocal<?> key, Object value) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    // 根據 ThreadLocal 的散列值,查找對應元素在數組中的位置
    int i = key.threadLocalHashCode & (len-1);
    // 採用“線性探測法”,尋找合適位置
    for (ThreadLocal.ThreadLocalMap.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),說明之前的ThreadLocal對象已經被回收了
        if (k == null) {
            // 用新元素替換陳舊的元素
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // ThreadLocal對應的key實例不存在也沒有陳舊元素,new 一個
    tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
    int sz = ++size;
    // cleanSomeSlots 清楚陳舊的Entry(key == null)
    // 如果沒有清理陳舊的 Entry 並且數組中的元素大於了閾值,則進行 rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

   ThreadLocalMap的set方法和Map的put方法差不多,但是有一點區別是:put方法處理哈希衝突使用的是鏈地址法,而set方法使用的開放地址法。

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

private final int threadLocalHashCode = nextHashCode();

   threadLocalHashCode是ThreadLocal的散列值,定義爲final,表示ThreadLocal一旦創建其散列值就已經確定了,生成過程則是調用nextHashCode():

private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

    nextHashCode表示分配下一個ThreadLocal實例的threadLocalHashCode的值,HASH_INCREMENT則表示分配兩個ThradLocal實例的threadLocalHashCode的增量。

getEntry()

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

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

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

    這裏有一個重要的地方,當key == null時,調用了expungeStaleEntry()方法,該方法用於處理key == null,有利於GC回收,能夠有效地避免內存泄漏。在Java知音公衆號內回覆“面試題聚合”,送你一份面試寶典

get()方法

public T get() {
    // 獲取當前線程
    Thread t = Thread.currentThread();
    // 獲取當前線程的成員變量 threadLocal
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 從當前線程的ThreadLocalMap獲取相對應的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 獲取目標值        
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

    首先通過當前線程獲取所對應的成員變量ThreadLocalMap,然後通過ThreadLocalMap獲取當前ThreadLocal的Entry,最後通過所獲取的Entry獲取目標值result。

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

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

set(T value)

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

     獲取當前線程所對應的ThreadLocalMap,如果不爲空,則調用ThreadLocalMap的set()方法,key就是當前ThreadLocal,如果不存在,則調用createMap()方法新建一個,如下:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

initialValue()

protected T initialValue() {
    return null;
}

   該方法定義爲protected級別且返回爲null,很明顯是要子類實現它的,所以我們在使用ThreadLocal的時候一般都應該覆蓋該方法。

注意:如果想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法。

因爲在上面的代碼分析過程中,我們發現如果沒有先set的話,即在map中查找不到對應的存儲,則會通過調用setInitialValue方法返回i,而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認情況下,initialValue方法返回的是null。

remove()

 public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

該方法的目的是減少內存的佔用。當然,我們不需要顯示調用該方法,因爲一個線程結束後,它所對應的局部變量就會被垃圾回收。

ThreadLocal使用示例

public class SeqCount {
    private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
        // 實現initialValue()
        public Integer initialValue() {
            return 0;
        }
    };
    public int nextSeq(){
        seqCount.set(seqCount.get() + 1);
        return seqCount.get();
    }
    public void removeSeq(){
        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() {
            for(int i = 0 ; i < 3 ; i++){
                System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
            }
            seqCount.removeSeq();
        }
    }
}

結果如下:

Thread-1 seqCount :1
Thread-3 seqCount :1
Thread-2 seqCount :1
Thread-0 seqCount :1
Thread-2 seqCount :2
Thread-3 seqCount :2
Thread-1 seqCount :2
Thread-3 seqCount :3
Thread-2 seqCount :3
Thread-0 seqCount :2
Thread-1 seqCount :3
Thread-0 seqCount :3

ThreadLocal與內存泄漏

爲什麼會出現內存泄漏

首先看一下運行時ThreadLocal變量的內存圖:

運行時,會在棧中產生兩個引用,指向堆中相應的對象。

可以看到,ThreadLocalMap使用ThreadLocal的弱引用作爲key,這樣一來,當ThreadLocal ref和ThreadLocal之間的強引用斷開 時候,即ThreadLocal ref被置爲null,下一次GC時,threadLocal對象勢必會被回收。

這樣,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,如果當前線程再遲遲不結束的話,比如使用線程池,線程使用完成之後會被放回線程池中,不會被銷燬,這些key爲null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成內存泄漏。

其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap裏所有key爲null的value。

但是這些被動的預防措施並不能保證不會內存泄漏:

  • 使用static的ThreadLocal,延長了ThreadLocal的生命週期,可能導致的內存泄漏。

  • 分配使用了ThreadLocal又不再調用get(),set(),remove()方法,那麼就會導致內存泄漏。

爲什麼要使用弱引用?

使用弱引用,是爲了更好地對ThreadLocal對象進行回收。如果使用強引用,當ThreadLocal ref = null的時候,意味着ThreadLocal對象已經沒用了,ThreadLocal對象應該被回收,但由於Entry中還存着這對ThreadLocal對象的強引用,導致ThreadLocal對象不能回收,可能會發生內存泄漏。

爲什麼不將value也設置成弱引用?

爲什麼呢?

如何避免內存泄漏?

每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

ThreadLocal與髒讀

前面說了,ThreadLocal中的set()、get()和remove()方法都會對key==null的value進行處理,其中set()和get()方法是將key==null的value置爲null。但是如果ThreadLocal是static類型的,並且配合線程池使用,線程池會重用Thread對象,同時會重用與Thread綁定的ThreadLocal變量。倘若下一個線程不調用set()方法重新設置初始值,也不調用remove()方法處理舊值,直接調用get()方法獲取,就會出現髒讀問題。

例子如下。

public class DirtyDataInThreadLocal {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        //使用固定大小爲1的線程池,說明上一個線程屬性會被下一個線程屬性複用
        ExecutorService pool = Executors.newFixedThreadPool(1);
        for(int i = 0; i < 2; i++){
            MyThread thread = new MyThread();
            pool.execute(thread);
        }
    }
    private static class MyThread extends Thread{
        private static boolean flag = true;
        @Override
        public void run() {
            if(flag){
                //第一個線程set後,沒有remove,第二個線程也沒有進行set操作
                threadLocal.set(this.getName() + ", session info.");
                flag = false;
            }
            System.out.println(this.getName() + " 線程是 " + threadLocal.get());
        }
    }
}

打印結果如下:

Thread-0線程是 Thread-0, session info.
Thread-1線程是 Thread-0, session info.

ThreadLocal使用場景

數據連接和Session管理

最常見的ThreadLocal使用場景爲 用來解決 數據庫連接、Session管理等。

如:

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};
public static Connection getConnection() {
    return connectionHolder.get();
}

 

private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章