13-J.U.C之ThreadLocal

1. ThreadLoacal 是什麼

       該類提供了線程局部 (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中。

       下圖是ThreadThreadLocalThreadLocalMap的關係:

在這裏插入圖片描述

2. 使用示例

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 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());
            }
        }
    }
}

在這裏插入圖片描述

3. ThreadLocal源碼解析

       ThreadLocal雖然解決了這個多線程變量的複雜問題,但是它的源碼實現卻是比較簡單的。ThreadLocalMap是實現ThreadLocal的關鍵,我們先從它入手。

3.1 ThreadLocalMap

       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;
    }
}

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

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

3.1.1 set(ThreadLocal key, Object value)

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();
}

       這個set()操作和我們在集合瞭解的put()方式有點兒不一樣,雖然他們都是key-value結構,不同在於他們解決散列衝突的方式不同。集合Mapput()採用的是拉鍊法,而ThreadLocalMapset()則是採用開放定址法。

      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的增量,從nextHashCode就可以看出他們的定義。

3.1.2 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回收,能夠有效地避免內存泄漏。

3.2 ThreadLocal

3.2.1 get()

      返回當前線程所對應的線程變量。

public T get() {
    // 獲取當前線程
    Thread t = Thread.currentThread();

    // 獲取當前線程的成員變量 ThreadLocalMap 
    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 getMap(Thread t) {
    return t.threadLocals;
}

在這裏插入圖片描述

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

3.2.2 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,如果不爲空,則調用ThreadLocalMapset()方法,key就是當前ThreadLocal,如果不存在,則調用createMap()方法新建一個,如下:

	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

3.2.3 initialValue()

      返回該線程局部變量的初始值。

	protected T initialValue() {
	    return null;
	}

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

3.2.4 remove()

      將當前線程局部變量的值刪除。

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

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

4. ThreadLocal 爲什麼會內存泄漏

      前面提到每個Thread都有一個ThreadLocal.ThreadLocalMapmap,該mapkeyThreadLocal實例,它爲一個弱引用,我們知道弱引用有利於GC回收。當ThreadLocal的key=null時,GC就會回收這部分空間,但是value卻不一定能夠被回收,因爲他還與Current Thread存在一個強引用關係,如下:

在這裏插入圖片描述
      由於存在這個強引用關係,會導致value無法回收。如果這個線程對象不會銷燬那麼這個強引用關係則會一直存在,就會出現內存泄漏情況。所以說只要這個線程對象能夠及時被GC回收,就不會出現內存泄漏。如果碰到線程池,那就更坑了。

      那麼要怎麼避免這個問題呢?
      在前面提過,在ThreadLocalMap中的setEntry()getEntry(),如果遇到key = null的情況,會對value設置爲null。當然我們也可以顯示調用ThreadLocalremove()方法進行處理。

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