【線程】ThreadLocal 剖析 (十四)

我的原則:先會用再說,內部慢慢來。
學以致用,根據場景學源碼


一、概念

ThreadLocal的作用是提供線程內的局部變量。這種變量在線程的生命週期內起作用,減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度。

二、整體架構

2.1 代碼架構

2.1.1 Thread.class
public class Thread implements Runnable {
	...
    ThreadLocal.ThreadLocalMap threadLocals = null;

    // 這個下次講解
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
	...
}

於是,我們可以知道,每個thread的內部,都有一個 threadLocals變量,這是一個 Map,裏面就存儲着KV對。

2.1.2 ThreadLocal.class
public class ThreadLocal<T> {
		//注意 ThreadLocal 只有這麼一個變量!!
	private final int threadLocalHashCode = nextHashCode();  
	// 服務於上面的 threadLocalHashCode
	private static AtomicInteger nextHashCode = new AtomicInteger(); 
	private static int nextHashCode() { //底層是 CAS +1獲取hashCode
	    return nextHashCode.getAndAdd(HASH_INCREMENT);
	}

	static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
		@Override
        protected T initialValue() {
            return supplier.get();
        }
	}
	static class ThreadLocalMap {
		static class Entry extends WeakReference<ThreadLocal<?>> {

		}
		private void set(ThreadLocal<?> key, Object value) {}
		private void remove(ThreadLocal<?> key) {}
		private Entry getEntry(ThreadLocal<?> key) {}
	}
	
	//第一次#get的時候,調用 #setInitialValue,再調用 #initialValue
	protected T initialValue() {return null;}
    public void set(T value) {}
    public T get() {}
    private T setInitialValue() {}
    public void remove() {}
	public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}
	...
}

2.2 UML

  • ThreadLocal

  • ThreadLocal.ThreadLocalMap

  • ThreadLocal.ThreadLocalMap.Entry
    在這裏插入圖片描述

  • 總結一下:

  1. 每一個thread對象內部有 1 個 ThreadLocal.ThreadLocalMap,
  2. ThreadLocal.ThreadLocalMap 內部放了個 Entry[] 數組,
  3. 每個 Entry 對象存儲了一個key和一個value。
  4. 每個 key 都是一個 threadLocal 對象 (balance和cost屬於兩個 threadLocal對象,對應了2個value,threadLocal對象其實就是一個線程內變量)
  5. 對象 ThreadLocal1 ,ThreadLocal2 ,ThreadLocal3 類似與託管機構。Thread相當與人。

2.3 對象關係圖

在這裏插入圖片描述

=== 點擊查看top目錄 ===

三、代碼 Demo

舉個例子:每個人都有一個賬戶,每次買東西都會進行扣費,每個人的賬戶存款都不一樣。一個人就是一條線程,賬戶存款就是線程內的局部變量。(如何設置成全局變量,那麼就是一個錢包全部人花,這不符合場景。

//每個人都有一個賬戶,每次買東西都會進行扣費,每個人花的是自己的錢,每個人的賬戶存款都不一樣。一個人就是一條線程,賬戶存款就是線程內的局部變量。
public class _22_TestThreadLocal {
    public static void main(String[] args) {
        wallet wallet = new wallet();
        // 3.  3個線程共享wallet,各自消費
        new Thread(new TaskDemo(wallet), "A").start();
        new Thread(new TaskDemo(wallet), "B").start();
        new Thread(new TaskDemo(wallet), "C").start();
    }

    private static class TaskDemo implements Runnable { //一個人就像一個線程
        private wallet wallet;

        public TaskDemo(wallet wallet) {
            this.wallet = wallet;
        }

        public void run() {
            try{
                for (int i = 0; i < 3; i++) {
                    wallet.spendMoney();

                    // 4. 每個線程打出3個
                    System.out.println(Thread.currentThread().getName() + " --> balance["
                            + wallet.getBalance() + "] ," + "cost [" + wallet.getCost() + "]");
                }
            }finally {
                wallet.removeAll();
            }

        }
    }

    private static class wallet { //一個人就像一個線程
        // 1.通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
        private static ThreadLocal<Integer> balance = ThreadLocal.withInitial(new Supplier<Integer>() {
            @Override
            public Integer get() {
                return 100;
            }
        }); // 假設初始賬戶有100塊錢

        private static ThreadLocal<Integer> cost = ThreadLocal.withInitial(() -> 0); // 假設初始賬戶有100塊錢
//        private static ThreadLocal<Integer> balance = new ThreadLocal<>();
//        private static ThreadLocal<Integer> cost = new ThreadLocal<>();

        public int getBalance() {
            return balance.get();
        }

        public int getCost() {
            return cost.get();
        }

        // 2。 消費
        public void spendMoney() {
            int balanceNow = balance.get();
            int costNow = cost.get();
            balance.set(balanceNow - 10); // 每次花10塊錢
            cost.set(costNow + 10);
        }


        public void removeBalance(){
            balance.remove();
            balance = null;
        }

        public void removeCost(){
            cost.remove();
            cost = null;
        }

        public void removeAll(){
            removeBalance();
            removeCost();
        }
    }
}

輸出:

A --> balance[90] ,cost [10]
B --> balance[90] ,cost [10]
A --> balance[80] ,cost [20]
C --> balance[90] ,cost [10]
B --> balance[80] ,cost [20]
C --> balance[80] ,cost [20]
A --> balance[70] ,cost [30]
C --> balance[70] ,cost [30]
B --> balance[70] ,cost [30]

結論:
線程 A,B,C 內部的變量balance ,cost 互不干擾。

=== 點擊查看top目錄 ===

四、源碼分析

threadLocal.class 結構

4.1 withInitial() 方法,初始化

  • 看代碼初始化
    傳入 Supplier,實例化ThreadLocal對象,用的是ThreadLocal的子類SuppliedThreadLocal。
// 1.通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<Integer> balance = ThreadLocal.withInitial(new Supplier<Integer>() {
    @Override
    public Integer get() {
        return 100;
    }
}); // 假設初始賬戶有100塊錢
  • withInitial 方法
public static <S> ThreadLocal<S> ThreadLocal#withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

=== 點擊查看top目錄 ===

4.2 get() 方法,獲取

  • demo使用:
public void spendMoney() {
    int balanceNow = balance.get();
    int costNow = cost.get();
    balance.set(balanceNow - 10); // 每次花10塊錢
    cost.set(costNow + 10);
}
public T get() {
    Thread t = Thread.currentThread();
4.2.1    ThreadLocalMap map = getMap(t); // 獲取這個 thread 的 Map
    if (map != null) { //一開始就是null
4.2.2        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue(); //====  初始化這個 ThreadLocalMap,存儲在thread內部,然後再返回對應的V
}
4.2.1 getMap() 方法
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals; // 獲取這個 thread 的 Map, 一開始是null
    }
4.2.2 getEntry() 方法
//當前 thread 調用 getEntry 方法,輸入 ThreadLocal對象,先得到Entry[] tables,再得到數組中的Entry對象(KV)
private Entry ThreadLocal.ThreadLocalMap#getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key) // e==null說明這個位置已經被清除了。 e.get() != key 說明key
        return e;
    else
4.2.3        return getEntryAfterMiss(key, i, e);
}
4.2.3 getEntryAfterMiss() 方法

走到這裏,說明 e == null || e.get() != key

  1. e == null ,該 key 已經被垃圾回收了
  2. e.get() != key,e.get() 很有可能是null,說明是髒數據了
java.lang.ThreadLocal.ThreadLocalMap#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) // 如果是 髒Entry,清理掉
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

=== 點擊查看top目錄 ===

4.3 setInitialValue() 方法,設置初始值 (設置並獲取value默認值)

  • setInitialValue
private T setInitialValue() {
    T value = initialValue(); //====  初始化數值,如果沒使用 withInitial 方法,那麼返回 null
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); //記住 Map 是存儲在 thread對象裏面的,一開始也是 null
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value); //=== 一開始thread對象裏面沒這個map,是null,走這裏
    return value;
}

=== 點擊查看top目錄 ===

4.4 initialValue() 方法(map中的value默認值)

  • 父類 ThreadLocal
protected T ThreadLocal#initialValue() {
    return null;
}
  • 子類 SuppliedThreadLocal
protected T SuppliedThreadLocal#initialValue() {
    return supplier.get(); //返回生產者的默認值。
}

=== 點擊查看top目錄 ===

4.5 createMap() 方法 (給thread對象初始化 threadLocals變量)

void ThreadLocal#createMap(Thread t, T firstValue) {
 	//注意這裏傳入的是this,也就是最外面的 balance 對象(ThreadLocal的實例化對象)
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

注意 new ThreadLocalMap(this, firstValue),這裏傳入的是this,也就是最外面的 balance 對象(ThreadLocal的實例化對象)
在這裏插入圖片描述
=== 點擊查看top目錄 ===

4.5.1 看下 ThreadLocalMap 的構造方法
java.lang.ThreadLocal.ThreadLocalMap#ThreadLocalMap(java.lang.ThreadLocal<?>, java.lang.Object)

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { 
	// 初始化 ThreadLocal.ThreadLocalMap#table,這是一個數組
    table = new Entry[INITIAL_CAPACITY]; 
     // & 操作,這個實際上是一個取餘數操作 。
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 記住,一開始不是往0丟,而是由余數確定。之後,挨個往後放
    table[i] = new Entry(firstKey, firstValue); 
    size = 1; //初始化的時候,長度是1
    setThreshold(INITIAL_CAPACITY); // 設置閾值,這個擴容的時候會用到
}
4.5.1 看下變量 threadLocalHashCode
public class ThreadLocal<T> {
	//注意 ThreadLocal 只有這麼一個變量!!
	private final int threadLocalHashCode = nextHashCode();  
	// 服務於上面的 threadLocalHashCode
	private static AtomicInteger nextHashCode = new AtomicInteger(); 
	private static int nextHashCode() { //底層是 CAS +1獲取hashCode
	    return nextHashCode.getAndAdd(HASH_INCREMENT);
	}
	...
}

=== 點擊查看top目錄 ===

4.6 set 方法

=== 總體Set流程圖 ===

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

  • ThreadLocal.ThreadLocalMap#set
private void ThreadLocal.ThreadLocalMap#set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table; 
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 取餘數

	// 這個for是向右進行掃描,要麼替換value,要麼去掉髒數據如果滿足條件,那麼return
    for (Entry e = tab[i];
         e != null; //如果被佔用了,那麼才進入這個 for 
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // 1. 如果 threadlocal 對象一樣,那麼就重置 value ,返回
            e.value = value;
            return; //直接結束方法
        }

        if (k == null) { // 2. 如果 threadlocal 是 null,那麼找到髒數據了 
4.6.1            replaceStaleEntry(key, value, i);
            return;//直接結束方法
        }
    }

    tab[i] = new Entry(key, value);  // 正常是走到這裏,設置kv
    int sz = ++size; 
    /*
    	cleanSomeSlots 清楚掉key是null,也就是 threadlocal是null的情況。有這情況返回 true
    	cleanSomeSlots 返回 false,並且 目前的長度+1 超過閾值 threshold了,那麼進行 rehash
    	注意: cleanSomeSlots 傳入的參數是 sz,也就是table數組內的真實存放Entry長度。 size <= len
     */
4.6.2    if (!cleanSomeSlots(i, sz) && sz >= threshold) 
4.6.3        rehash();
}

=== 點擊查看top目錄 ===

4.6.1 replaceStaleEntry()方法

// 進入這個方法的前提是:該下標 staleSlot的 key == null,是個髒數據,需要處理
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    //第一個髒entry
    int slotToExpunge = staleSlot;

    // 1. 這個for的目的,就是找到最前面的髒Entry的下標
1.   for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null; 
         i = prevIndex(i, len))
        if (e.get() == null) //如果 key 是 null,那麼就是髒數據
            slotToExpunge = i;  //髒的下標就是你啦,不斷往前推進

    // Find either the key or trailing null slot of run, whichever
    // occurs first
2.   for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
2.1    if (k == key) {

        	//如果在向後環形查找過程中發現key相同的entry就覆蓋,並且和髒entry進行交換
            e.value = value;

2.2            tab[i] = tab[staleSlot];
2.3            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
2.4            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            //搜索髒entry並進行清理
2.5            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
2.6        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
3.    tab[staleSlot].value = null;
4.    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
5.    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

這個 replaceStaleEntry 方法比較複雜,先分成兩種情況。後期節點中,有沒有出現 k == key(2.1) 的情況,

  1. 無 k == key 的情況:

    情形A1:假如前無髒數據,後無髒數據。
    情形A2:假如前有髒數據,後無髒數據。
    情形A3:假如前無髒數據,後有髒數據。
    情形A4:假如前有髒數據,後有髒數據。

  2. 假如出現了 k == key 的情況:

    情形B1:假如前無髒數據,後無髒數據。
    情形B2:假如前有髒數據,後無髒數據。
    情形B3:假如前無髒數據,後有髒數據。
    情形B4:假如前有髒數據,後有髒數據。

4.6.1.1 無 k == key 的情況
  • 情形A1:假如前無髒數據,後無髒數據。
	跳過1,
	那麼直接走代碼 3,4 覆蓋 staleSlot 下標的 Entry
	不用進行cleanSomeSlots!!!
  • 情形A2:假如前有髒數據,後無髒數據。
走代碼1 slotToExpunge  往前移
那麼走完34之後,會走5,進行 cleanSomeSlots

在這裏插入圖片描述

  • 情形A3:假如前無髒數據,後有髒數據。
跳過1,
那麼走代碼2,slotToExpunge 往後移
那麼走完34之後,會走5,進行 cleanSomeSlots
(2.6 條件滿足)

在這裏插入圖片描述

  • 情形A4:假如前有髒數據,後有髒數據。
走代碼1 ,slotToExpunge  往前移
走代碼2的時候,由於兩個 if 都不滿足,
那麼走完34之後,會走5,進行 cleanSomeSlots.
(2.6 條件不滿足)

==== 情況同A2 ===

4.6.1.2 有 k == key 的情況
  • 情形B1:假如前無髒數據,後無髒數據。
跳過1,
走2.1 ,把 entry 覆蓋,交換 entry 引用,髒Entry放到了 i 處(2.2 處)。 髒Entry那個地方指向了tab[i] 的Entry(2.3 處),
2.4條件滿足,slotToExpunge 往後推到i 的位置(前面都是不髒的而且 key 不重複的),然後執行 cleanSomeSlots 。
return
(2.4,2.6 條件滿足)

在這裏插入圖片描述

  • 情形B2:假如前有髒數據,後無髒數據。
走代碼1 ,slotToExpunge  往前移
走代碼2的時候,由於兩個 if 都不滿足,
那麼走完34之後,會走5,進行 cleanSomeSlots.
(2.6 條件不滿足)

在這裏插入圖片描述

  • 情形B3:假如前無髒數據,後有髒數據。
走代碼1 ,slotToExpunge  往前移
走代碼2的時候,由於兩個 if 都不滿足,
那麼走完34之後,會走5,進行 cleanSomeSlots.
(2.6 條件不滿足)

在這裏插入圖片描述

  • 情形B4:假如前有髒數據,後有髒數據。
走代碼1 ,slotToExpunge  往前移
走代碼2的時候,由於兩個 if 都不滿足,
那麼走完34之後,會走5,進行 cleanSomeSlots.
(2.6 條件不滿足)

=== 清理情形同 B2 ===

4.6.2 cleanSomeSlots() 方法
private boolean ThreadLocal.ThreadLocalMap#cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len); //爲什麼是nextIndex不是i,因爲i剛剛不才插入了一個Entry嗎,所以不可能是髒數據
        Entry e = tab[i];
        if (e != null && e.get() == null) { // 1. 首先Entry不是null,其次該Entry的key的null,那麼就定義成“髒數據”,進入expungeStaleEntry回收處理
            n = len; //2.重置N,重新再掃描一遍
            removed = true;
            i = expungeStaleEntry(i); // 3.清理髒數據,清除完,返回 Entry==null的下標。
        }
    } while ((n >>>= 1) != 0); //4. >>> 右移動1位,初始狀態是 16 (二進制:1000)
    return removed;
}
  1. 如果一切順利的情況下,不會進入上面的1裏面的 if,也就是沒有髒數據要處理。那麼 n >>>= 1 每次右移動1位,16(二進制1000)移動4次就退出了。 log2(n)
  2. 如果中途遇到髒數據,那麼N重置爲 tables 的長度,那麼再循環 log2(n) 遍。
  3. 關注一下cleanSomeSlots變量N,承接一下上下文。set方法中cleanSomeSlots(i, sz) 傳入的參數是 sz,也就是table數組內的真實存放Entry長度。 size <= len. 如果在 cleanSomeSlots 的過程中遇到髒數據,那麼 n 變大成 len。那麼搜索範圍log2(n)也增大

在這裏插入圖片描述

4.6.3 rehash 方法
private void ThreadLocal.ThreadLocalMap#rehash() {

    expungeStaleEntries();// 清除不乾淨的Entity,也就是 key == null的 Entity對象

    if (size >= threshold - threshold / 4) //table數組長度大於四分之三的時候就擴容
        resize();
}

=== 點擊查看top目錄 ===

4.6.4 expungeStaleEntries 方法
// 清理一堆髒數據 (key == null)
 private void ThreadLocal.ThreadLocalMap#expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

=== 點擊查看top目錄 ===

4.6.5 expungeStaleEntry 方法
// 清理單個髒數據 (key == null)
private int ThreadLocal.ThreadLocalMap#expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    // 2. 抹掉下標是 staleSlot 的Entry
    tab[staleSlot].value = null; 
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    //2.往後環形繼續查找,直到遇到 table[i]==null時結束
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
     	//3. 如果在向後搜索過程中再次遇到髒entry,同樣將其清理掉 ( k == null 說明是髒的entity)
        if (k == null) { 
            e.value = null;
            tab[i] = null;
            size--;
        } else {
        	//處理rehash的情況
            int h = k.threadLocalHashCode & (len - 1);  //取餘,len肯定是2的N次方
            if (h != i) { // h!=i ,意思就是目前存儲的有問題,i 必須等於 h 纔對
                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;
}

也就是說該 expungeStaleEntry 方法,清理掉當前髒entry後,並沒有閒下來繼續向後搜索,若再次遇到髒entry繼續將其清理,直到哈希(table[i])爲null時退出。table[i] 爲null,說明這個位置還沒被佔用呢

=== 點擊查看top目錄 ===

4.7 remove 方法

4.7.1 ThreadLocal#remove 方法
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

將某個線程從 ThreadLocalMap 中去除,底層是調用 ThreadLocal.ThreadLocalMap#remove 方法

4.7.2 ThreadLocal.ThreadLocalMap#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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
4.7.3 Reference#clear 方法
  • java.lang.ref.Reference#clear
    public void clear() {
        this.referent = null;
    }

指針指向null,讓GC好回收剛剛指向的對象。

=== 點擊查看top目錄 ===

五、番外篇

下一章節:【線程】ThreadLocal 內存泄漏問題(十五)
上一章節:【線程】CountDownLatch 內部原理(十三)

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