ThreadLocal 原始碼分析

wang03發表於2021-07-04

1、ThreadLocal 原始碼分析

  1. 在多執行緒開發中,我們經常會使用ThreadLocal來避免共享變數的競爭,提高效率。ThreadLocal底層到底是怎麼實現的呢,今天就帶大家一起來看看它底層實現。另外也會隨便分析下網上討論比較多的關於ThreadLocal記憶體洩漏等等究竟是怎麼一回事

    我本地的jdk版本是11.0.8,不同版本的jdk,threadLocal原始碼實現可能有差別,不過大致是一樣的。

  2. 首先看下我們一般都是怎麼使用ThreadLocal的

​ 這是一段使用threadLocal的demo程式碼

public class ThreadLocalDemo {
    public final static ThreadLocal<String> threadLocal = new ThreadLocal<>() {
        @Override
        protected String initialValue() {
            return "initValue";
        }
    };

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println("init Value =" + threadLocal.get());
            threadLocal.set("abc");
            System.out.println("執行其他邏輯");
            String str = threadLocal.get();
            System.out.println(str);
            threadLocal.remove();
        }).start();
        Thread.currentThread().join();
    }
}

​ 這段程式碼比較簡單,不用具體說threadLocal的這些方法都是幹啥的了。我們直接順著main方法裡面的呼叫順序一起去看看這些呼叫背後都是怎麼實現的。

  • 如果直接把類的原始碼粘上來,做分析,感覺太零散了,看不到方法之間的呼叫關係,所以,這次就準備按照呼叫邏輯,一步一步來分析了


  • 首先我們main方法是在第11行呼叫了threadLocal.get(),這是我們第一次主動呼叫threadLocal的地方,那我們先從這裡進去

    
//這就是ThreadLocal的get方法了,我們泛型引數是String,所以這裡的T也就是String了,下面我們一行一行根據我們上面的Demo來分析下這個程式碼
    public T get() {
        //這一行就不用說了,Thread.currentThread()就是獲取當前執行緒
        Thread t = Thread.currentThread();
        
        //getMap(t)這行是幹什麼呢?我們這個方法的程式碼比較簡單,我就直接貼上到下面了 
        
    	//ThreadLocalMap getMap(Thread t) {
        //	return t.threadLocals;
    	//}
        //上面這3行就是getMap(t)呼叫的程式碼了,比較簡單,獲取執行緒t上的threadLocals屬性,這個屬性是什麼東西呢?我們再看看這個屬性
        
        //ThreadLocal.ThreadLocalMap threadLocals = null;
        //上面這一行就是在Thread類中定義的threadLocals屬性了,看樣子是ThreadLocal類中的內部類ThreadLocalMap的一個例項,具體是啥,我們先不仔細看了,繼續看後面程式碼吧  
        ThreadLocalMap map = getMap(t);
        
        //這裡返回的map當前是null,因為Thread的threadLocals,從來沒有初始化過
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //因為map==null,所以會呼叫到這個setInitialValue這個方法,從這裡返回,我們繼續看看這個方法吧
        return setInitialValue();
    }

	
    private T setInitialValue() {
        //這裡的initialValue方法是protected的,預設返回null,由於我們上面Demo第4行重寫了initialValue方法,所以這裡的呼叫就是我們上面的程式碼,這裡的返回應該是上面我們Demo第5行的"initValue"
        T value = initialValue();
        //這幾行程式碼和上面get方法是一致的
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //map==null
        if (map != null) {
            map.set(this, value);
        } else {
            //會走到這裡來,我們去這個createMap方法看看
            createMap(t, value);
        }
        //這裡的this是通過匿名繼承的ThreadLocal的,不會走到這個instanceof內部去
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        //我們上面的Demo第4行的threadLocal.get()呼叫最終就會從這裡返回,返回值是"initValue"
        return value;
    }
	//這裡就是初始化Thread的threadLocals屬性了,後面這個屬性就不是null了,這裡建立了個ThreadLocalMap物件,有兩個引數,第一個this就是我們Demo中呼叫threadLocal.get()方法的物件,也就是Demo第4行的threadLocal物件,firstValue就是上面的字串"initValue"。
//我們進到ThreadLocalMap構造方法去看看
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
//
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //這裡首先會建立一個Entry的陣列,INITIAL_CAPACITY = 16;也就是這裡建立一個大小是16的Entry陣列,Entry是啥,我們下面再看,先看看這個構造方法的其他幾行程式碼
            table = new Entry[INITIAL_CAPACITY];
            //threadLocalHashCode是ThreadLocal的成員變數,
            
            // private final int threadLocalHashCode = nextHashCode();
            //上面是它的定義,從上面可以看到每次建立ThreadLocal物件時就會初始化threadLocalHashCode,它的值是通過靜態方法nextHashCode()賦值的,每呼叫一次nextHashCode返回值就在上次的基礎上增加0x61c88647。
            //這裡的i就是threadLocalHashCode的值和(INITIAL_CAPACITY - 1)進行與運算,這裡的(INITIAL_CAPACITY - 1)值是15,計算結果在0-15之間,作為陣列的下標
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //這裡建立個Entry物件,賦值給table中下標為i的
            table[i] = new Entry(firstKey, firstValue);
            //這裡的size是table陣列中元素的個數
            size = 1;
            //這裡是設定threshold = len * 2 / 3;當size的值超過threshold時,table陣列就會擴容成原來陣列的2倍
            setThreshold(INITIAL_CAPACITY);
        }
//這裡我們看看Entry物件
//這就是Entry的原始碼了,更簡單。繼承了WeakReference物件,這裡會把構造方法的ThreadLocal入參包裝成弱引用。具體啥是弱引用,下面貼上上一段《深入理解java虛擬機器》上面的描述。放到我們這裡來說,就是ThreadLocal變數在沒有其他地方引用(只有在Entry這裡有引用),當下次垃圾回收的時候,就會被回收掉
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
			//
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

下面是《深入理解java虛擬機器》上面的關於引用的描述


到這裡上面Demo第4行的原始碼就全部看完了,下面簡單總結下執行邏輯。

  1. 首先進入ThreadLocal的get方法,獲取當前執行緒的threadLocals變數,我們這個變數沒有初始化過,所以這個變數為空,繼續執行setInitialValue()方法,並從這裡返回
  2. 在setInitialValue方法中首先呼叫我們Demo中重寫ThreadLocal的initialValue方法,獲取返回值。
  3. 繼續呼叫createMap方法,
    • 建立ThreadLocalMap物件,內部建立Entry陣列,將threadLocal物件包裝成弱引用及initialValue方法的返回值建立Entry物件,填充到Entry陣列陣列中
    • 將建立的ThreadLocalMap物件賦值給當前執行緒的threadLocals變數
  4. 將2中獲取的值返回

現在我們看下Demo中的第12行threadLocal.set("abc")

//這就是ThreadLocal的set方法了,和get方法差不多
    public void set(T value) {
        //獲取當前執行緒
        Thread t = Thread.currentThread();
        //獲取當前執行緒的threadLocals的屬性,在上面get方法已經設定過這個屬性了 ,所以這裡不為空了
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //map不為null,就會走到這裡了,這裡的this就是我們Demo中繼承ThreadLocal的匿名內部類,value就是"abc",下面我們重點去看下這個方法
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
   
   //這個方法是ThreadLocal的內部類ThreadLocalMap的。
   private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.
			//這個是ThreadLocalMap構造方法建立的table,是Entry陣列
            Entry[] tab = table;
            int len = tab.length;
       		//這個是獲取根據key計算在陣列中的下標,考慮到有可能兩個key計算出來的是同一個i,所以陣列中下標i不一定就是我們需要的key,會從當前i的下標向後遍歷。同樣根據key獲取值的時候,也會有類似情況
            int i = key.threadLocalHashCode & (len-1);
			//獲取下標i的值Entry,進行遍歷,直到entry.key==我們的入參key或者entry==null或者entry.key==null(這個場景就是key其他地方沒有引用了,只有Entry有對應的弱引用,在下次垃圾回收後,entry.key就會==null)的情況下,結束迴圈
       
       //注意:這裡不會出現陣列中所有下標都滿了,且entry.k!=key的場景,因為下面的後面會判斷size>=threshold,進行擴容
            for (Entry e = tab[i];
                 e != null;
                 //考慮到可能會有衝突,也就是上面說的兩個key計算出來的是同一個i,所以key有可能不存在對應下標i的位置
                 //nextIndex(i, len)向後獲取下一個位置是迴圈的,如果達到i==len-1;這時i=0;就會從陣列頭元素開始,後面幾個方法說的向前遍歷,向後遍歷都是類似的
                 e = tab[i = nextIndex(i, len)]) {
                //獲取Entry中的key,這裡是個弱引用,通過get方法獲取弱引用的實際物件。
                ThreadLocal<?> k = e.get();
				//找到了對應的key,就設定新值,返回
                if (k == key) {
                    e.value = value;
                    return;
                }
				//k==null,這個場景說了,就是外部沒有對ThreadLocal物件的其他引用了(強引用),GC釋放後,k就是null,jiu就會走到這裡
                if (k == null) {
                    //這個方法會從當前下標i開始,刪除過期的entry,重新設定新的Entry到i的位置
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			//走到這裡,這時下標i對應的Entry已經是null了
            tab[i] = new Entry(key, value);
       		//陣列中元素個數+1
            int sz = ++size;
       		//這裡會移除一些過期的entry,判斷sz>= threshold,如果成立,就擴容陣列
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }


//這個方法主要是

 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
     //這裡的for迴圈都不會陷入死迴圈,因為上面有 sz >= threshold
     	//獲取Entry陣列
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            int slotToExpunge = staleSlot;
     //從當前入參staleSlot開始向前遍歷,直到下標i對應的Entry==null
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                //隨著i向前遍歷,slotToExpunge的值會逐步更新,直到staleSlot和它之前第一個tab[i]==null之間,e.get()==null的下標,如果threadLocal物件外部一直有引用,那e.get()==null,就不會為null,也不會走到這個if分支
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
     //從當前入參staleSlot開始向後遍歷,直到下標i對應的Entry==null
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                //獲取到對應的Entry的threaLocal變數
                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.
                //如果k==key,這時就是更新value就可以了
                if (k == key) {
                    e.value = value;
					//這裡會進行元素交換,把找到的Entry換到入參staleSlot的位置
                    //注意當前staleSlot元素位置是過期的,需要清理的,交換後i下標位置的元素就是需要清理的
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //這個條件成立的話,需要第一個for迴圈中if分支沒有進入,也就是不存在staleSlot和它之前第一個tab[i]==null之間,e.get()==null
                    if (slotToExpunge == staleSlot)
                        //走到這裡說明staleSlot之前到tab[i]==null之間沒有無效元素,我們上面進行了元素交換,這時i就是第一個過期的元素了
                        slotToExpunge = i;
                    //清理無效的元素,slotToExpunge就是staleSlot之前到tab[i]==null到之後到tab[i]==null這段元素之間第一個過期元素的下標位置
                    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.
                //k==null說明當前陣列已經有entry失效了,slotToExpunge == staleSlot說明staleSlot之前沒有失效的,這時就要清理後面的過期元素
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
     		//入參的staleSlot下標已經個個過期的值,將value設定為null,重新賦值個新的entry
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                //走到這裡說明staleSlot向前遍歷或者向後遍歷中出現了k==null,這時需要清理過期的entry
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
//從staleSlot位置開始,直到陣列中entry==null。清理在這過程中陣列過期的元素,並調整那些元素有效,但是下標位置不正確的元素位置
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //清空對應staleSlot對應的元素,size-1
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            //在staleSlot開始向後遍歷,直到陣列中entry==null
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //如果k==null,就清空元素,size-1
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //h是演算法計算出來元素應該在陣列中的下標位置
                    int h = k.threadLocalHashCode & (len - 1);
                    //i是元素的直接儲存下標,由於碰撞的原因元組有可能不是存在演算法計算出來的下標位置
                    if (h != i) {
                        //h!=i說明元素根據演算法出來的下標和實際儲存下標不一致,這時由於我們清空了一些過期元素,這時就需要重新調整這些有效的,演算法計算出來的下標,和實際儲存下標不一致的元素位置
                        tab[i] = null;
						
                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        //從h開始向後遍歷,找到陣列中的null位置,將當前下標i位置上的元素設定到對應位置
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            //這裡的i就是for迴圈中退出條件,staleSlot開始向後遍歷,陣列中第一個entry==null的位置
            return i;
        }
//從i開始清理無效的元素     
//注意下標i的位置不是無效元素,要不下標i位置==null,要不是有效元素
		private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                //獲取i後面的位置
                i = nextIndex(i, len);
                Entry e = tab[i];
                //如果i位置的元素是過期的,就執行清理
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    //這裡上面說的清理過期元素,調整位置不正確的有效元素的位置
                    i = expungeStaleEntry(i);
                }
                //這裡的n>>>=1主要是掃描次數,應該是出於效率的考慮
            } while ( (n >>>= 1) != 0);
            return removed;
        }
//這就是擴容了      
		private void rehash() {
            //這裡首先清理陣列中所有的過期元素,同時會調整不正確元素的下標
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            //由於我們上面會清理過期元素,所以size有可能變小,有可能就不需要擴容了,所以這裡重新判斷是否需要擴容,如果需要就進行擴容
            if (size >= threshold - threshold / 4)
                resize();
        }

//這裡就是擴容了,陣列長度變成原來的2倍,重新設定threshold和size
        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            //長度設定成原來的2被
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (Entry e : oldTab) {
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    //繼續清理擴容過程中過期的元素
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        //在新陣列中設定元素位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

上面就是Demo第12行 threadLocal.set("abc")方法涉及的所有程式碼了 。

下面總結下:

1.獲取當前執行緒的threadLocals變數,如果不存在就呼叫createMap(t, value),建立並設定值,我們Demo第11行threadLocal.get()就走的這裡

2.如果當前執行緒的threadLocals變數存在,就呼叫map.set(this, value)設定值

3.在獲取值的過程中首先根據呼叫者threadLocal物件計算出應該儲存在陣列中的下標,

  • 如果當前下標對應的陣列元素是null,就新生成Entry元素放入陣列下標i的位置,陣列size+1,判斷是否需要擴容,如果需要就對陣列進行擴容
  • 如果當前下標對應的陣列元素不是null,就獲取對應位置的元素進行判斷,如果entry.key == 我們呼叫的threalocal物件,就更新value返回。如果entry.key==null,說明元素釋放了就呼叫replaceStaleEntry進行處理。如果這兩種情況都不是,那就繼續從當前下標開始向後遍歷,繼續判斷處理

最後看下Demo中第16行threadLocal.remove();

//獲取當前執行緒的threadLocals變數,如果之前呼叫過get,set方法,那這時這個變數就不是null了
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             //走到這裡看看移除元素的方法,這裡的this,是我們方法的呼叫者,也就是threadLocal例項物件
             m.remove(this);
         }
     }
        //這個方法就比較簡單了,計算threadLocal下標,從這個位置開始向後遍歷,對應元素(這裡使用的是key==,所以找到的肯定是同一個物件),將Entry中引用的threadLocal物件清空,再執行清理過期元素的動作
        
        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;
                }
            }
        }


上面remove方法也講完了,這個比較簡單。就是找到t.threadLocals中對應的元素Entry,呼叫e.clear()清理掉entry引用的threadLocal變數(這時,通過e.get()獲取的threadLocal元素就是null了),然後呼叫expungeStaleEntry執行過期元素的清理。


2、日常使用注意

上面就是我們日常使用threadLocal中方法的原始碼了 ,通過上面程式碼對於呼叫的內部細節我們也基本看到了,下面說一些日常使用過程中需要注意的地方。

它們的結構簡單畫個圖,就是下面的樣子了

文章開始的Demo只是一些日常使用,通過上面的原始碼閱讀,我們其實還是可以看到一些其他的使用方式和注意地方

  • 我們一般是在多個執行緒中是使用同一個threadLocal物件,其實我們也可以在一個執行緒中使用多個threadLocal物件(同樣多個執行緒也就可以使用多個threadLocal物件),就像下面這樣
       //建立多個ThreadLocal物件
		ThreadLocal<String> threadLocal0 = new ThreadLocal<>();
        ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        new Thread(() -> {
            //在一個執行緒中使用多個threadLocal物件
            //通過上面原始碼我們看到threadLocal物件只是用來尋找當前執行緒中threadLocals變數中陣列的位置,並讀取或者設定值。由於查詢元素下標用的是==,所以無論怎麼找到的都是同一個物件例項
            threadLocal0.set("th0");
            threadLocal1.set("th1");
            System.out.println(threadLocal1.get());
            System.out.println(threadLocal0.get());
        }).start();
        Thread.currentThread().join();
  • 記憶體釋放相關

    上面的原始碼分析部分,我們看到了儲存到threadLocals中Entry的threadLocal變數是弱引用了,而弱引用在下次gc的時候就會被清理,threadLocal被清理了,上面的清理過期元素的方法就會把對應Entry進行清理。但是這裡有個前提,threadLocal只有弱引用的時候才會被清理,如果有強引用存在,就不會被清理。

    看下面的例子

        ThreadLocal<String> threadLocal0 = new ThreadLocal<>();
        new Thread(() -> {
         	//我們第一行定義的地方,threadLocal物件是個強引用,只要這個強引用存在,threadLocals中對應的Entry就不會被清理
            threadLocal0.set("th0");
            //這個也很好理解,如果被清理掉了,那我們這裡的get方法就獲取不到值了,我們見過之前set後,對應get獲取不到值的情況嗎,肯定沒有麼
            System.out.println(threadLocal0.get());
            //所以我們想要清理存放的元素Entry就需要呼叫threadLocal0.remove()進行清理,或者是呼叫 threadLocal0=null;方法,這樣當前 threadLocal0就只有執行緒threadLocals中對應的弱引用,這時,gc才會清理掉entry.k,對應元素才會在清理元素方法中清理掉
        }).start();
        Thread.currentThread().join();

另外由於ThreadLocal的操作都是基於當前執行緒的threadLocals變數的,如果當前執行緒不存在了,被清理掉了 ,只要threadLocals變數和內部Entry的key和value,如果沒有其他地方進行引用,也都會被gc清理掉。

以上就是關於ThreadLocal的全部內容了。


另外父執行緒和子執行緒之間傳遞引數可以通過inheritableThreadLocals,這個變數的設定實在Thread的構造方法中,和threadLocals一樣,都是ThreadLocal.ThreadLocalMap的例項。這個用的不多,這裡就不說了。

相關文章