ThreadLocal原理

我們通常用ThreadLocal來實現線程局部變量的存儲。在許多開源框架中ThreadLocal被廣泛使用。這篇文章來討論一下ThreadLocal的實現原理

一、ThreadLocal的實現原理

ThreadLocal的實現原理其實很簡單,我們先來看一下ThreadLocal常用的幾個方法:

public void set(T value)
public void remove()
public T get()

通過set、get、remove這個三個方法,可以實現線程局部變量的添加、獲取、刪除。
首先我們先來看一下set方法的實現:

  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 getMap(Thread t) {
        return t.threadLocals;
    }

我們可以看到set方法是將對象value保存在當前線程的threadLocals這個ThreadLocalMap中,以當前的ThreadLocal對象作為map的鍵值。ThreadLoccaMap是ThreadLocal類的一個內部類,我們先不管它的實現,先來看一下ThreadLocal的get方法和remove方法的實現。

get方法:
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

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

我們可以看到,當當前線程的ThreadLocalMap對象存在的時候,返回值從這個對象中獲取,這個和set方法保存value是相對應的,都是從當前線程保存的ThreadLocalMap對象中存儲和獲取。當ThreadLocalMap對象不存在的時候返回 setInitialValue() 返回的對象。這個方法我們可以看到:通過 initialValue()方法獲得一個value對象,這個方法默認返回一個null,留給子類實現,用來初始化一個用來保存的對象的默認值。然后將這個對象放在當前線程的ThreadLocalMap中(如果不存在ThreadLocalMap對象就創(chuàng)建一個)。

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

依然是調用ThreadLocalMap的remove方法。
好了,究竟ThreadLocalMap是什么?我們接下來看一下:
其實ThreadLocalMap是一個Map,它的實現和HashMap類似,我們先來看一下用來保存K-V的節(jié)點:

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

我們先不用管WeakReference是什么,我們只需要知道,Entry 保存了k和v。
接下來看一下ThreadLocalMap的set方法的實現:

        private Entry[] table;

        private void set(ThreadLocal<?> key, Object value) {
            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)]) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                    e.value = value;
                    return;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

ThreadLocalMap的set方法和早期HashMap的實現類似,都是先計算哈希,然后確定hash槽的位置,不同的是,ThreadLocalMap通過數組存儲K-V對象(Entry),而HashMap是通過散列表存儲K-V對象。ThreadLocalMap首先獲得存儲數組的長度,然后通過hash算法計算要設置的節(jié)點所在的哈希槽的位置,如果哈希槽的位置沒有元素,就新創(chuàng)建一個Entry對象放在這里。如果有元素,就判斷該元素的k是否和當前要設置的k相等,如果是就將這個哈希槽存儲的entry對象的value重新賦值;如果k是空的話,說明這個ThreadLocal對象被手動設置為null了,是無效的。就把這個節(jié)點替換掉,具體怎么實現看一下replaceStaleEntry這個方法,這里不贅述。如果當前哈希槽位置有合法元素,并且k不和要保存的k相等,就去下一個哈希槽的位置重復檢查,下一個哈希槽的位置是這個方法計算的:

  private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

添加完元素之后,會去判斷當前存儲數組內元素的數量是否超過了threshold,我們可以叫threshold為擴容因子,threshold = len * 2 / 3。當超過擴容因子的時候就去檢查并且移除壞節(jié)點。移除壞節(jié)后,如果size >= threshold - threshold / 4,就要真正的擴容。我們看一下這個方法:

private void rehash() {
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

expungeStaleEntries()這個方法,從方法名上可以看出這個方法的作用:去除壞掉的Entries。什么是壞掉的Entries呢?我們可以看一下這個方法的實現:

        private void 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);
            }
        }

當一個節(jié)點不是null時,調用節(jié)點的get()方法,如果說得的結果是null,這個節(jié)點就是壞的節(jié)點。Entry的get方法其實是他父類WeakReference<T>的父類Reference<T>的方法。這兩個類是什么呢?對JVM有了解的小伙伴應該對這兩個類不陌生,我們知道,在java中,對象的引用分為四種:強引用、軟引用、弱引用、虛引用。引用強度逐漸減弱。
強引用是我們常見的對象引用,比如:Object o = new Object();
只要一個對象被強引用所引用,就不會被垃圾收集器回收。當內存不足時,jvm會拋出OOM異常。
軟引用對應著Reference<T>的實現類SoftReference<T>,這種引用引用的對象不會立刻被回收,但是當內存空間不足的時候,垃圾收集器就會回收軟引用所引用的對象。
弱引用對應著Reference<T>的實現類WeakReference<T>,當軟引用指向的對象被垃圾收集器發(fā)現后,就會回收這個對象(只有軟引用引用這個對象,如果強引用同時也引用這個對象時,這個對象并不會被回收)。
虛引用:也叫幽靈引用,虛引用主要用來跟蹤對象被垃圾回收器回收的活動。不會影響任何垃圾回收的過程。
回到前面的所說的判斷是否為壞的節(jié)點,e.get()所獲得的其實是e存儲的key,也就是ThreadLocal對象。所以我們可以看出,Thread中的ThreadLocalMap并不會影響ThreadLocal在jvm中的生命周期。當一個節(jié)點被判定為壞節(jié)點后,這個節(jié)點就會被移除,具體實現我們看一下expungeStaleEntry這個方法:

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            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;
        }

首先將位置為staleSlot處存儲的Entry的對象的value也設置為null,然后將這個對象從存儲的數組中移除,并將size減1。然后一次檢查下一個位置(nextIndex(staleSlot, len)確定下一個位置)是否為壞節(jié)點,是的話就移除,否則重新計算這個節(jié)點所在的位置,將這個節(jié)點移動到計算后的新位置。這樣做的原因是因為在set節(jié)點的時候,如果存在hash沖突,并且key不相等時,會將調用nextIndex(staleSlot, len)方法重新確定hash槽的位置。
真正的擴容方法是由resize()方法實現的,實現過程是很簡單。我們看一下具體實現:

        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                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;
        }

我們可以看到,每次擴容大小都是原來大小的2倍,擴容的過程就是新建一個大小為原來2倍的數組,將原來數組內的元素放到新數據中。過程很簡單這里就不在贅述。

二、疑問,線程池中,大量任務使用ThreadLocal會不會造成OOM

根據上面的分析,ThreadLocal實現線程局部存儲是通過每個線程Thread中的ThreadLocalMap存儲以ThreadLocal對象為key,以要存儲的對象為value來實現的。而ThreadLocalMap中,存儲K-V是通過Entry實現,Entry繼承了WeakReference。所以ThreadLocalMap不會影響ThreadLocal對象的在內存中的回收。通過之前《java線程池淺析》這篇我們可以知道線程池實現的原理其實是多個(或一個)線程執(zhí)行提交的runnable任務。runnable任務中使用ThreadLocal,當runnable任務結束(其實是run方法結束),runnable任務中使用的ThreadLocal就會失去和GCroot的連接,這個時候只有ThreadLocalMap中的Entry會引用該ThreadLocal對像,所以當內存不足的時候,ThreadLocal對像會被回收。所以在線程池中,這種ThreadLocal的使用是不會造成OOM的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容