解讀ThreadLocal與Java實戰

一 前言

在多線程併發的學習中,我們總會接觸到一個類,即是ThreadLocal。這個類的作用是提供線程內的局部變量,換句話來說,就是提供一個能與當前線程綁定的局部變量。這個變量能夠在多線程併發的環境下保證每個線程中變量的獨立性。

只要線程處於活動狀態並且Threadocal實例可以訪問,每個線程就擁有對其線程局部變量副本的隱式引用;在一個線程消失之後,線程本地實例的所有副本都會被垃圾收集(除非存在對這些副本的其他引用)

我們可以看下該類的操作:

public class ThreadLocalTest {
    private static ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        integerThreadLocal.set(3);
        System.out.println(integerThreadLocal.get());
        integerThreadLocal.remove();
        System.out.println(integerThreadLocal.get());
    }
}

輸出爲:

3
null

那麼效果如何呢,看下面這個例子:

public class ThreadLocalTest {
    private static ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        LocalThread thread1 = new LocalThread();
        thread1.setValue(3);
        thread1.start();

        LocalThread thread2 = new LocalThread();
        thread2.setValue(4);
        thread2.start();
    }

    public static class LocalThread extends Thread {
        int value;

        public void setValue(int value) {
            this.value = value;
        }

        @Override
        public void run() {
            integerThreadLocal.set(value);
            for (int i = 0; i < 2; i++) {
                System.out.println(integerThreadLocal.get());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

輸出結果爲:

4
3
3
4

從中我們可以看出兩個線程中對於ThreadLocal類中的變量是不一樣的。

下面讓我們從源碼的角度來看一下這個類

二 源碼解析

2.1 基礎字段

先從基礎部分看起:

	// hash code
    private final int threadLocalHashCode = nextHashCode();

    // 原子性操作類型
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    // 每次增加的hash code
    private static final int HASH_INCREMENT = 0x61c88647;

    // 下一個hash值
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

從上面的定義可以知道,ThreadLocalthreadLocalHashCode是從0開始,每新建一個ThreadLocal,對應的hashcode就加0x61c88647。值得注意的是控制hashcode增加的三個方法或字段爲類方法或類字段,而代表當前實例對象hashcode的字段則是threadLocalHashCode

生成hash code間隙爲0x61c88647這個魔數,可以讓生成出來的值較爲均勻地分佈在2的冪大小的數組中。對應的十進制爲1640531527。均勻分佈的好處在於很快就能探測到下一個臨近的可用slot,從而保證效率。

2.2 默認值

ThreadLocal類中也提供了默認值的設置,如下:

	// 爲ThreadLocal設置默認的get初始值,需要重寫
    protected T initialValue() {
        return null;
    }

    // 創建一個線程局部變量。通過在Supplier上調用get方法確定變量的初始值
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
	static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;

        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }

Supplier類是一個只有get()方法的消費式函數式接口,通過SuppliedThreadLocal繼承了ThreadLocal類,從而實現了對於initialValue()方法的覆蓋。

具體的使用方法如下:

	// 此處輸出爲3而不是null
	public static void main(String[] args) {
        Supplier supplier = () -> 3;
        System.out.println(ThreadLocal.withInitial(supplier).get());
    }

2.3 ThreadLocalMap的定義

ThreadLocal類的內部維護了一個靜態內部類:

static class ThreadLocalMap {
}

ThreadLocalMap是一個自定義哈希映射,僅用於維護線程本地變量值。每個線程都有一個ThreadLocalMap類型的threadLocals變量。

此處爲Thread類中的字段:

ThreadLocal.ThreadLocalMap threadLocals = null;

在創建與ThreadLocal關聯的Map時,便會將當前線程與對應的Map關聯起來:

	// 創建與ThreadLocal關聯的Map。在InheritableThreadLocal中重寫。
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

在其內部中最核心的實現應該是Entry靜態類:

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

            Entry(ThreadLocal<?> k, Object v) {
                // 將ThreadLocal註冊成爲弱引用,避免將ThreadLocal置爲null時,因爲其仍保持與Entry的關聯而導致ThreadLocal不能被垃圾收集器回收
                super(k);
                value = v;
            }
        }

Entry的key爲ThreadLocal,value爲ThreadLocal對應的值。

關於弱引用的資料可以查看:https://www.cnblogs.com/absfree/p/5555687.html

其基礎字段爲:

// 初始化容量,必須爲2的冪
        private static final int INITIAL_CAPACITY = 16;

        // 可增長的緩存數組
        private Entry[] table;

        // table中entries的數量
        private int size = 0;

        // 下次擴容的閾值
        private int threshold; // Default to 0

        // 設置調整大小閾值以保持最差的2/3負載係數。
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        // 增加i並取餘len
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        // 減少i並取餘len
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

其構造函數也可以先看下:

		ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            // 取hash code
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 利用firstKey與firstValue構建一個新的Entry
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            // 設置擴容閥值,取傳入值得2/3
            setThreshold(INITIAL_CAPACITY);
        }

        // 構造一個新映射,包括給定父映射中的所有可繼承ThreadLocals。僅由createInheritedMap調用。
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            // 遍歷父映射
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    // 取出哈希槽中當前Entry的key
                    // 此處的e.get()方法是Reference類中的方法,返回此引用對象的引用對象
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    // key不爲空即代表對應線程的ThreadLocal未被清除
                    if (key != null) {
                        // 此處的childValue(e.vale)方法爲InheritableThreadLocal覆蓋實現
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        // 取hash code
                        int h = key.threadLocalHashCode & (len - 1);
                        // 線性探查法解決衝突
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

其他部分則留到後面講解

值得注意到的是,此處第二個構造函數的訪問限定符爲private,那麼在ThreadLocal裏面是由哪個類來調用他的呢:

	// 工廠方法創建繼承本地線程的映射。旨在僅從Thread構造函數調用。
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

大家可能會問,一個Thread類擁有一個threadLocals來存放ThreadLocalMap,那這個構建的類是來幹嘛的, 那讓我們看看到底是哪裏會調用這個方法:

答案是在Thread類中的init()方法中會調用該方法,並將該方法的返回值放置到以下字段:

	// 與此線程相關的InheritableThreadLocal值。此映射由InheritableThreadLocal類維護。
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

通過這個可以滿足開發者子線程獲得父線程ThreadLocal的需求

2.4 get()方法

	// 返回當前線程副本的值,如果沒有存在則使用initialValue設置默認值並返回
    public T get() {
        Thread t = Thread.currentThread();
        // 通過當前線程得到對應的map
        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;
            }
        }
        // 返回默認值,無則爲null
        return setInitialValue();
    }

此處就是之前說過的與Thread類關聯起來的字段,在LocalThread的每個實例中都可以根據這個當前線程得到相應的map:

	// 獲取與ThreadLocal關聯的Map。
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

並且該ThreadLocalMap類是一個靜態類,說明任何實例化ThreadLocal或其子類的對象,他們所存放的map都是同一個。

		private Entry getEntry(ThreadLocal<?> key) {
            // hash取址
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                // 使用線性探查法解決hash衝突
                return getEntryAfterMiss(key, i, e);
        }

此處的獲取get()調用了Reference類裏面的方法:

	// 返回該引用對象的引用
    public T get() {
        return this.referent;
    }

首先根據key的hash code找到在table中對應的Entry,若找到則返回e,若沒有找到則可能是因爲發生衝突而導致與當前線程關聯的entry存放在了後面,此時調用getEntryAfterMiss(key, i, e);方法繼續尋找。此處使用了Hash表中的線性探查法。

		// 在直接散列槽(direct hash slot)中找不到密鑰時使用的getEntry方法的版本。
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                // 此時相當於找到當前線程對於的entry
                if (k == key)
                    return e;
                // 此處說明map的key已被置爲null,但value還在,此時需要解決清除掉已經失效的條目
                if (k == null)
                    // 若執行該方法後,tab[i]不爲null,即是說明在i後面的數據被移動到i處,此時可以繼續尋找,否則停止尋找
                    expungeStaleEntry(i);
                else
                    // 增加i並對len取餘,繼續尋找
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

從指定的位置開始不斷向下查找,如果此時找到了當前線程對應的entry則直接返回。若查找到null,在entry存在的情況下key爲null說明該數據是一個髒數據,需要清除掉已經失效的條目。若處理後該處仍爲null,則說明後續已無元素,此時便可以結束循環返回null。若處理後該處不爲null,則說明在i後面的數據被移動到i處,此時可以繼續尋找。

這裏可以清楚地瞭解到expungeStaleEntry(i)方法是怎樣清除掉已經失效的條目:

		// 通過重新處理staleSlot和下一個空槽之間的任何可能發生衝突的條目來清除陳舊條目。這也會消除在尾隨空值之前遇到的任何其他陳舊條目。
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 除當前髒entry
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            // 持續往後查找entry[],直到遇到table[i]==null時結束,並且返回此時的i
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                // 如果在向後搜索過程中再次遇到髒entry,同樣將其清理掉
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    // 處理rehash的情況,相當於將Entry e重新放置到離h最近的位置,也可以說是i之前。
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // 與Knuth 6.4算法R不同,我們必須掃描到null,因爲可能有多個條目已過時。
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

該方法主要做了這麼幾件事情:

  1. 清理當前髒entry,即將其value引用置爲null,並且將table[staleSlot]也置爲null。
  2. 從當前staleSlot位置向後(nextIndex)繼續搜索,直到遇到哈希桶(tab[i])爲null的時候退出;
  3. 若在搜索過程再次遇到髒entry,繼續將其清除。

此處是隻針對一段哈希桶(tab[i])不爲null的片段進行操作,並沒有涉及到所有哈希槽。

若在map中沒有發現以當前線程爲key的map,則調用setInitialValue()方法來設置初始化值:

	// 用於建立initialValue,返回initialValue的值
    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;
    }

根據方法名理解起來都不困難,無非是存在map則設置爲爲默認值,否則創造map

map的set方法在下面set()方法的部分再進行講解,此時我們先來看下createMap(t, value);

	// 創建與ThreadLocal關聯的Map。在InheritableThreadLocal中重寫。
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

使用了ThreadLocalMap的構造函數。

2.5 set()方法

	// 設置當前線程對應的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);
    }

裏面的調用方法在方法處都講過一遍了,此處簡單地過一下在get()方法中出現過的方法調用:

getMap(t):拿取了當前線程的threadLocal變量。

createMap(t, value):調用了ThreadLocalMap的構造函數。

此處的set(ThreadLocal<?> key, Object value)方法是在ThreadLocalMap中實現的private方法:

		// 設置與key關聯的值
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            // 緩存數組長度
            int len = tab.length;
            // hash code
            int i = key.threadLocalHashCode & (len-1);

            // 從i處開始線性探查相應的key
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                // 找到了則使新值替換舊值
                if (k == key) {
                    e.value = value;
                    return;
                }

                // 遇到髒entry,此時通過replaceStaleEntry進行處理
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            // 插入新的entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            // 若此時沒有移除任何一個髒數據且當前entry存放數量到達閥值則rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

此處我們能夠看到插入新的entry後是會對map進行擴容的,即不存在map內部全爲非null的情況,因此環形查找一定會在某一個點終止循環。

在該方法中針對髒entry做了這樣的處理:

  1. 如果當前table[i]!=null的話說明hash衝突就需要向後環形查找,若在查找過程中遇到髒entry就通過replaceStaleEntry進行處理;
  2. 如果當前table[i]==null的話說明新的entry可以直接插入,但是插入後會調用cleanSomeSlots方法檢測並清除髒entry

當遇到髒數據的時候使用replaceStaleEntry(key, value, i)方法清除髒數據,並且將指定值插入到指定鍵的位置:

		// 將設置操作期間遇到的髒entry替換爲指定鍵的entry。無論是否已存在指定鍵的entry,傳入value參數的值將被存儲在entry中。
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // 向前找到第一個髒entry
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

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

                // 如果在向後環形查找過程中發現key相同的entry就覆蓋並且和髒entry進行交換
                if (k == key) {
                    // 修改值
                    e.value = value;
                    // 將該entry與前方置空的hash Slot互換
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // 如果向前未搜索到髒entry,並且在查找過程中還未發現髒entry,那麼就以當前位置作爲cleanSomeSlots的起點
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // 搜索髒entry並進行清理
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                //如果向前未搜索到髒entry,則在查找過程遇到髒entry的話,後面就以此時這個位置作爲起點執行cleanSomeSlots
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 如果在查找過程中沒有找到可以覆蓋的entry,則將新的entry插入在髒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)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

由於該方法中存在向前環形查找髒entry與向後查找可替換entry,因此可以分爲四種情況來進行講解:

  • 向前查找到了髒entry

    • 向後查找到了可覆蓋的entry

      一開始slotToExpunge初始狀態與staleSlot,當向前環形查找到了髒entry時,則將slotToExpunge更新爲當前髒數據的索引,直到遇到table[i] == null時前向搜索結束。接下來的for循環中進行了後向環形查找,如果找到了可覆蓋的entry,則先覆蓋當前位置i中的entry,然後再與staleSlot位置上的髒entry進行交換。

      					e.value = value;
                          // 將該entry與前方置空的hash Slot互換
                          tab[i] = tab[staleSlot];
                          tab[staleSlot] = e;
      

      交換後i處的位置就是髒數據了。最後使用cleanSomeSlots方法以slotToExpunge爲起點開始清理髒entry。

      此處之所有不用slotToExpunge與可覆蓋entry進行交換的原因是:該map中的鍵值對是進行環形查找的,因此slotToExpunge不一定會在指定key的hash的後面,而staleSlot因爲一開始就是因爲插入時候才被發現是髒數據,因此直接與其交換是沒有問題的。

    • 向後沒有查找到可覆蓋的entry

      一開始slotToExpunge初始狀態與staleSlot,當向前環形查找到了髒entry時,則將slotToExpunge更新爲當前髒數據的索引,直到遇到table[i] == null時前向搜索結束。接下來的for循環中進行了後向環形查找,如果沒有找到可覆蓋的entry並且後向環形查找過程結束。則將新插入的entry直接放在staleSlot處。

      			tab[staleSlot].value = null;
                  tab[staleSlot] = new Entry(key, value);
      

      最後使用cleanSomeSlots方法以slotToExpunge爲起點開始清理髒entry。

  • 向前環形查找沒有查找到髒entry

    • 向後查找到了可覆蓋的entry

      若在前向環形查找的整個過程中都沒有遇到髒entry,則slotToExpunge的初始狀態依舊與staleSlot相同。接下來的for循環進行環形查找,若此時遇到了髒entry,則更新slotToExpunge的位置爲當前位置i。

      				if (k == null && slotToExpunge == staleSlot)
                          slotToExpunge = i;
      

      若查找到了可覆蓋的entry,則先覆蓋當前位置的entry,然後再與staleSlot位置上的髒entry進行交換。

      			e.value = value;
      			tab[i] = tab[staleSlot];
      			tab[staleSlot] = e;
      

      最後使用cleanSomeSlots方法以slotToExpunge爲起點開始清理髒entry。

    • 向後環形查找沒有可覆蓋的entry

      若在前向環形查找的整個過程中都沒有遇到髒entry,則slotToExpunge的初始狀態依舊與staleSlot相同。接下來的for循環進行環形查找,若此時遇到了髒entry,則更新slotToExpunge的位置爲當前位置i。 並且將slotToExpunge更新到當前位置。

      			if (slotToExpunge == staleSlot)
      				slotToExpunge = i;
      

      最後使用cleanSomeSlots方法以slotToExpunge爲起點開始清理髒entry。

以上便是replaceStaleEntry方法中的所有情況。下面我們再看一下是怎樣使用cleanSomeSlots方法來清理髒entry的:

首先是一開始調用的expungeStaleEntry(slotToExpunge)方法,會清除指定staleSlot後面table[i]不爲null範圍內的一系列髒entry。最後返回的是清除髒entry後遇到的第一個entry爲null的索引,具體可以看上文對其的分析。

		// 嘗試掃描並處理一些髒entry。當添加新元素或另一個髒entry被清除時,將調用此方法。
        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                // 清除髒entry
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

在調用該方法的情況中,索引i所指向的entry可能是一個空entry或者剛剛放入一個新entry,反正肯定不是一個髒entry。

參數n的用處主要用於掃描控制(scan control),從while中是通過n來進行條件判斷的說明n就是用來控制掃描趟數(循環次數)的。在掃描過程中,如果沒有遇到髒entry就整個掃描過程持續log2(n)次,log2(n)的得來是因爲n >>>= 1,每次n右移一位相當於n除以2。如果在掃描過程中遇到髒entry的話就會令n爲當前hash表的長度(n=len),再掃描log2(n)趟,注意此時n增加無非就是多增加了循環次數從而通過nextIndex往後搜索的範圍擴大。

不是很清楚爲什麼使用n >>>= 1來判斷掃描趟數,如果有懂的大神歡迎留言。。

如果是在set方法插入新的entry後調用,n位當前已經插入的entry個數size;如果是在replaceSateleEntry方法中調用,n爲哈希表的長度len。

如果有清理過髒entry則返回true,否則返回false。

最後的最後,如果將創建了一個新的entry並且此時不存在髒數據,且map中存儲的entry數量已經達到閥值時,便需要rehash了。

		private void rehash() {
            expungeStaleEntries();

            // 使用較低的閾值加倍以避免滯後
            if (size >= threshold - threshold / 4)
                resize();
        }

首先是清除髒entry:

		// 清除緩存數組中的所有髒entry
        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);
            }
        }

前面是判斷size >= 2 * threshold / 3,此處則判斷了size >= 3 * threshold / 4 。符合條件則調用resize():

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

            // 將舊map中的entry遷移到新entry上
            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.6 remove()方法

在上述的方法中,我們經常能夠看到在索引位置i中,Entry e = table[i],e != null && e.get() == null的情況發生,我們管這類數據爲髒數據,那他是怎樣出現的呢,讓我們繼續看下一個方法remove():

	// 移除當前線程對應的值
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
		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)]) {
                // 找到包含指定key的entry
                if (e.get() == key) {
                    // 清除,即將key置爲null,此時該entry變成了髒數據
                    e.clear();
                    // 清除髒數據
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
	public void clear() {
        this.referent = null;
    }

this.referent = null;是由於Entry繼承了WeakPerference,使用了referent來存儲key,將其置爲null便等同於將key置爲null

三 結論

與我們常用的Map不同,java裏大部分Map都是用鏈表發解決hash衝突的,而 ThreadLocalMap 採用的是開放定址法。使用均勻的hash算法能夠很好地配合開放定址法的工作,避免產生過多的衝突。而涉及到開放定址法肯定會有過期值清理的問題,在ThreadLocalMap中使用了expungeStaleEntryreplaceStaleEntrycleanSomeSlots三個方法來解決過期值的問題,防止內存泄漏。相較於拉鍊法來說使用開放定址法可以節省一些指針空間。

看到這裏可能有些同學會發現一個問題,get()、set()、remove()方法都對髒數據進行了處理,那麼這個髒數據是怎麼出現的呢?這個問題就涉及到了弱引用。一個ThreadLocal一般會有兩處地方對其進行引用,一個是線程在操作期間對其的強引用,另外一個是ThreadLocalMap對其的弱引用。但是在ThreadLocal中將ThreadLocalMap對其的引用設置爲弱引用,因此當線程在操作期間將ThreadLocal的實例強引用設爲null,該實例所對應的內存就會被回收,因此entry就存在key爲null的情況,無法通過一個Key爲null去訪問到該entry的value。

key是被回收了,但是value呢?由於此時通過這條引用鏈:當前線程–>threadLocalMap(線程中的threadLocals)–>entry–>value引用–>value內存。這時候的value無法被GC回收,這樣就造成了嚴重的內存泄漏。因此我們需要在操作get()、set()、remove()方法時都對髒數據進行了處理。

從這裏我們可以看出,threadLocal有可能存在內存泄漏,顯式地進行remove是個很好的編碼習慣,這樣是不會引起內存泄漏。

下面我們再看下threadLocalMap的生命週期:

下面是Thread類中的exit()方法,當線程退出時會執行該方法:

private void exit() {
    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    /* Aggressively null out all reference fields: see bug 4006245 */
    target = null;
    /* Speed the release of some of these resources */
    // 這裏兩段釋放了對threadLocalMap的引用
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
}

從中我們可以看出,當所有使用了ThreadLocal的線程退出時,ThreadLocalMap的實例便會被GC回收。

四 參考網站

https://www.javazhiyin.com/18072.html

https://www.jianshu.com/p/dde92ec37bd1

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