ThreadLocal源碼與設計思想深入分析

最近突然想到ThreadLocal雖然能夠爲每個線程提供一個變量的副本,實現線程之間變量操作的隔離性、互不影響。但是它卻不能保證狀態變量的線程安全性,也就是說如果ThreadLocal爲每個線程保存的變量原本就是線程不安全的,那麼在多線程環境下,對此變量的操作依然存在併發安全問題。並且ThreadLocal並不能實現父子線程之間變量的傳遞【它的子類InheritableThreadLocal能夠實現父子線程間的變量傳遞】。那麼爲什麼ThreadLocal不能保證以上兩點。接下來,就對ThreadLocal的實現深入瞭解下。

如何使用

源碼中給的例子如下:
此例爲每個線程生成一個唯一標識ID,線程標誌ID在第一次調用get()方法時,被初始化。然後再以後的調用過程中,依然保持不變。所以ThreadLocal建議的使用方法爲:定義一個全局的靜態不可變的對象,如果需要初始值,便通過匿名內部類重寫其initialValue()方法。

public class ThreadId {
    // 定義一個原子遞增的數據序列
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // 爲每個線程生成一個唯一ID
    private static final ThreadLocal<Integer> threadId =
        new ThreadLocal<Integer>() {
            protected Integer initialValue() {
               return nextId.getAndIncrement();
        }
    };

    // 返回當前線程的唯一ID
    public static int get() {
        return threadId.get();
    }
}

線程副本存儲機制

接下來講解源碼之前,有必要簡要闡述下ThreadLocal如何爲每個線程存儲線程隔離的副本以及他與線程的關係又是如何的。
查看Thread的源碼,你會發現他有兩個成員變量

/* 此成員變量存儲和線程有關的ThreadLocal值,數據的存儲結構爲散列Map。
不過這個Map並不是我們常用的HashMap,而是ThreadLocal類自己定義的一個
散列表。Map內部維護一個Entry數組,Entry就是key、value組合,Key就是構建的ThreadLocal變量,Value就是需要保存的變量,每個線程都會存儲這樣一個數據結構。下面會詳細講解。
*/
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* 此成員變量存儲和線程有關的InheritableThreadLocal值. 
* InheritableThreadLocal類是ThreadLocal的子類,它能實現父子線程之間
* 的值傳遞,而ThreadLocal不能.下面會詳細講解。
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

線程Thread便是通過這兩個成員變量,實現了線程間的變量隔離存儲,線程間對ThreadLocal變量的操作互不影響。它們之間的關係如下
圖1
             (圖一)

成員變量及魔數0x61c88647

    private final int threadLocalHashCode = nextHashCode();
    /**
     * 獲取一個原子遞增的序列,起始值爲0
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    /**
     * 此魔數極有可能是32位機器有符號數的黃金分割數.
     * 爲何如此選擇此數、可以參考黃金分割率、斐波那契散列法有關的資料
     * 源碼中註解的大致意思:用此魔數作爲步長與連續生成的哈希碼之間的差異-
     * 在長度爲2^N大小的哈希表中,
     * 將順序生成的thread-local的ID轉化爲近似最優擴展的乘法哈希值
     */
    private static final int HASH_INCREMENT = 0x61c88647;
    /**
     * 以魔數0x61c88647爲步長取得ThreadLocal的HashCode值.
     * 此方法會使ThreadLocal均勻分佈在稀疏哈希表中
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

關於此魔數是如何得出的,以下提供的思路,僅提供參考,並不保證可靠性。
筆者查閱了算法導論這本書【①】,在乘法散列法一節中可窺見一絲端倪。
乘法散列法的公式爲h(k)=「m(kA mod 1)」
此公式包含兩個步驟:
①用關鍵字乘上常數A(0<A<1),並提取kA的小數部分,即kA-「kA」
②用m乘以這個值,再向下取整
乘法散列法的一個優點就是對m的選擇不是特別關鍵,一般選擇它爲2的某個冪次(m=2^p,p爲某個整數),這是因爲我們可以在大多數計算機上,按下面所示方法較容易地實現散列函數。假設某計算機的字長爲w位,而k正好可用一個單字表示。限制A爲形如s/2^w的一個分數。其中s是一個取自0<s<2^w的整數,參見下圖,先用w位整數s=A*2^w乘上k,其結果是一個2w位的值,r1.2^w+r0,這裏r1爲乘積的最高位字,r0爲乘積的最低位字。所求的p位散列值中,包含了r0的p個最高有效位。
這裏寫圖片描述
雖然這個方法對於任何的A值都適用,但對某些值效果更好。最佳的選擇與待散列的數據特徵有關。Knuth[211]認爲A≈(√5-1)/2是個比較理想的值。
所以s=2^32*(√5-1)/2≈2654435769
如果把它轉爲32位的有符號整數則爲1640531527,這個數的十六進制數正是0x61c88647

public class ThreadHashTest {
  public static void main(String[] args) {
    long l1 = (long) ((1L << 31) * (Math.sqrt(5) - 1));
    System.out.println("as 32 bit unsigned: " + l1);
    int i1 = (int) l1;
    System.out.println("as 32 bit signed:   " + i1);
    System.out.println("MAGIC = " + 0x61c88647);
  }
}

方法

初始化

/**
 *返回當前線程的thread-local變量初始值。當線程第一次使用{@link #get}方法
 *訪問變量時,將調用此方法,除非線程先前調用了{@link #set}方法,
 *在這種情況下,{@code initialValue}方法將不會被線程調用。
 *通常,每個線程最多調用一次此方法,但如果調用{@link #remove}後,
 *再次調用{@link #get}則此方法會繼續執行。
 *此方法默認返回null,如果你希望初始化時,賦予ThreadLocal不同的值,
 *則需要重寫此方法,通常,示例中那樣,使用匿名內部類
 */
protected T initialValue() {
    return null;
}
/**
* JDK8新增方法
* 創建一個線程局部變量。 通過調用{@code Supplier}上的{@code get}方法
* 確定變量的初始值
* @since 1.8
*/
public static <S> ThreadLocal<S> withInitial(
    Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

/**
*你可以使用{@code withInitial}方法,如下初始化ThreadLocal變量
*/
private static final ThreadLocal<Integer> threadId    
   =ThreadLocal.withInitial(new Supplier<Integer>() {
        public Integer get() {
            return nextId.getAndIncrement();
        }
});

下面的方法,則是ThreadlLocal實現的核心了

設置當前線程的thread-local變量

public void set(T value) {
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取當前線程的成員變量ThreadLocalMap
    //此map維護着由一組ThreadLocal作爲key、給定變量值爲value的Entry數組
    ThreadLocalMap map = getMap(t);
    //如果map不爲空,則賦值【詳情見下面set()方法】
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

set()方法

/**
 * 此方法由靜態內部類ThreadLocalMap定義,大致流程如下:
 * 根據ThreadLocal的hashcode值,取得數組下標。以此下標開始往後尋找,
 * 1)如果此key已經存在,則直接替換value。
 * 2)如果key被垃圾回收,則繼續往後尋找是否存在相同的key,如果有則替換它,
 * 並交換兩者的位置、以保證哈希表的順序性。
 * 在此過程中,則儘可能的一次性清除無效槽位
 * 則以給定的key、value替換它。
 * 3)如果沒有找到key相同的槽位,則新建槽位。
 * 如果新建槽位後,表的size超過負載因子,則重新rehash(清除整個表的無效槽位)
 * 或者將整個表擴容爲2倍長度
 * @param key 爲ThreadLocal變量
 * @param value 給定值
 */
private void set(ThreadLocal<?> key, Object value) {

    //ThreadLocalMap維護的entry數組
    Entry[] tab = table;
    int len = tab.length;
    //threadLocal的hashcode值和數組長度-1做與運算
    //爲什麼用此算法求數組下標?見【註釋1】
    int i = key.threadLocalHashCode & (len-1);
    //此處循環有三個原因
    //1:查找key是否已經存在,如果存在則直接替換value
    //2:如果哈希存在碰撞,則繼續向後尋找未使用的槽位【即碰到null,則停止】
    //這是解決哈希碰撞常用的方式-開放尋址法-線性探查
    //3:因爲ThreadLocalMap的key是繼承自弱引用的ThreadLocal,
    //如果key被GC回收了、則entry也應該被清除,繼而保證散列表的順序
    //爲什麼ThreadLocalMap的key被設置爲弱引用,何時會被GC回收,見【註釋2】
    for (Entry e = tab[i];
         e != null;//碰到null,則停止
         e = tab[i = nextIndex(i, len)]) {

        ThreadLocal<?> k = e.get();
        //如果key相同,則直接替換value
        if (k == key) {
            e.value = value;
            return;
        }
        //key爲空,則以指定key和value替換它,然後再清除key爲空的槽位
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //如果沒有匹配到此key所在的槽位,則創建
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //如果沒有找到無效槽位並且數組中元素個數超過負載(16*2/3),則重新
    //擴容詳解,見下
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
/**
 * 數組下標i+1,如果超過數組長度,則重新從0開始。
 * 這正是開放尋址方法-線性探查方式,尋找槽位的序列順序
 * 爲什麼i增長到數組長度後,要從0開始?見【註釋3】線性探查序列
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
*以指定的key和value替換在set操作期間遇見到的無效槽位
*即key被垃圾回收了的Entry
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    //當碰到key被清空的Entry時,此時需要儘可能的清除整個entry。
    //當然,最好的策略應該是繼續往前尋找被GC了key,記住它的位置。
    //待後續一次性清除。但是因爲由魔數計算出來的hashcode是間歇性跳躍的。
    //例如0,3,6,9。
    //所以並不能保證一次清除所有,只能保證在遇到空槽之前儘量清除
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    //找到關鍵字所在的槽位或者後面緊跟的空槽
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 如果找到了此ThreadLocal所在的槽位,以給定的value替換舊value,
        //然後把此槽位與key爲空的槽位置換。以保證槽位的順序。
        //並清除key爲空的槽位
        if (k == key) {
            e.value = value;
            //把此槽位與key爲空的槽位置換
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            //如果最早出現key爲空的槽位仍然是參數中的staleSlot,
            //也就是說在staleSlot槽位之前,未發現陳舊的槽位
            //由於上面交換了槽位,所以需要刷新陳舊槽位的位置
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        //如果最早出現key爲空的槽位仍然是參數中的staleSlot,
        //也就是說在staleSlot槽位之前,未發現陳舊的槽位
        // 則重置陳舊槽位所在的位置
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 如果沒有匹配到此關鍵字,則重新設置
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果發現其它無效條目,請將其清除
    //清除詳解,見下面方法
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

 /**
 *刪除無效槽位
 *此處staleSlot即爲最早出現key爲空的entry的下標
 */
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;
    //一直往下探查,如果碰到key爲空的entry,則清除
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        //如果key爲空,則清除
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //如果因爲順序調整或陳舊條目清除而造成哈希重新分散。
            //因此爲了保證數組的順序性,則爲此key所在entry重新分配槽位
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    //返回遇到的空槽的位置
    return i;
}
/**
*此方法會儘可能的清除所有無效槽位
*當插入新元素或無效槽位被清除時,被執行
*當沒有無效條目被發現時,它執行數組長度的對數掃描次數(log2(n))
*當持續發現無效條目時,他執行log2(table.length)-1對數掃描次數
*這也會造成O(n)的時間複雜度
*/
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];
        if (e != null && e.get() == null) {
           //當發現無效槽位時,掃描起始對數的真數被重置爲N
           //因此極有可能會清除所有無效槽位
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);//無符號右移1位
    return removed;
}

重新散列或擴容

/**
 * 重新散列調整表格的大小,首先探查整個表、清除無效的槽位。
 * 如果這不足以縮小哈希表中元素的大小,則進行擴容
 */
private void rehash() {
    //清除哈希表中所有的無效槽位.
    expungeStaleEntries();

    // 降低負載因子、進行擴容。(將擴容操作提前)
    if (size >= threshold - threshold / 4)
        resize();
}
/**
 * 清除哈希表中所有的無效槽位.
 */
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)
            //清除無效槽位,同時對size做減法,詳解見上文
            expungeStaleEntry(j);
    }
}
/**
 * 將表容量加倍.
 */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    //創建double長度的新表
    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;
}

獲取當前線程的thread-local變量

/**
 * 返回當前線程的thread-local變量,如果ThreadLocal變量變量沒有被賦值,
 *那將會調用initialValue()方法,獲取初始值
 */
public T get() {
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取當前線程維護的ThreadLocalMap哈希表
    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;
        }
    }
    //如果未找到對應的變量,則執行初始化initialValue()方法,並返回初始值
    return setInitialValue();
}
/**
 * 根據key值,在ThreadLocalMap維護的數組內,尋找變量
 */
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //如果key相同,則直接返回變量。否則繼續尋找,直到遇到空槽位
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

/**
 * 當ThreadLocal變量的直接散列值未找到變量時,則執行此方法
 * 
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    //從當前元素開始,繼續向下探查,直到找到key對應的變量,或者遇到空槽位返回null
    //在探查的過程中,會清理無效槽位
    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;
}
/**
 * 執行初始化方法,可重寫ThreadLocal的initialValue()方法
 *然後指定set()方法,將初始值設置到當前線程的ThreadLocalMap變量裏
 * @return the initial value
 */
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //如果map存在,則直接設置。
    //如果不存在,則先創建,再設置
    //負載因子就在createMap()中設置
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
/**
*數組長度INITIAL_CAPACITY=16
*當數組的size大於16*2/3時,就擴容
*/
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);
}
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

移除當前線程的thread-local變量

/**
 * 移除當前線程thread-local變量,如果隨後便調用get()方法,
 * 則會執行初始化initialValue()方法,除非由set()設置當前線程變量
 */
 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }
/**
 * 首先調用clear()清除弱引用ThreadLocal變量,
 * 然後調用expungeStaleEntry(i)清除無效槽位,將Entry置爲null
 */
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;
        }
    }
}

爲什麼不能保證併發安全性


通過以上源碼、可以看出,Thread中ThreadLocal.ThreadLocalMap threadLocals變量,只是維護由ThreadLocal變量作爲key,任意變量作爲value的Entry數組。並未對Value做任何併發安全性操作。所以如果Value本事就是狀態對象,即使用ThreadLocal進行set後,使其變成線程私有變量,那麼它仍然存在併發安全隱患。

爲什麼InheritableThreadLocal能夠從父線程傳遞到子線程

InheritableThreadLocal繼承自ThreadLocal,重寫了下面三個方法

/**
*將父線程Entry的value複製到子線程(淺copy),
*可重寫
*/
protected T childValue(T parentValue) {
    return parentValue;
}

/**
 * 返回Thread的成員變量inheritableThreadLocals
 */
ThreadLocalMap getMap(Thread t) {
   return t.inheritableThreadLocals;
}

/**
*已給定的key、value初始化ThreadLocalMap
 */
void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

當新建線程,執行init()方法時,會執行下面一段代碼

Thread parent = currentThread();

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

再來看下ThreadLocal.createInheritedMap()方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
/**
*將父線程的Entry[]數組copy到子線程,如果開發者沒有重寫
*InheritableThreadLocal的childValue()方法,默認執行淺copy
*/
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) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                //默認執行淺copy,只複製引用
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

接下來,我們驗證下父子線程間的值傳遞是否爲淺複製

public class ThreadLocalTest {
    private static final InheritableThreadLocal<User> threadUser =
    new InheritableThreadLocal<User>();

    public static void main(String[] args) throws InterruptedException {
        threadUser.set(new User("parent"));
        System.out.println(Thread.currentThread().getName()+":"+threadUser.get().getName());
        new Thread(new Runnable() {
            public void run() {

                threadUser.get().setName("child");
                System.out.println(Thread.currentThread().getName()+":"+threadUser.get().getName());
            }
        }).start();
        TimeUnit.SECONDS.sleep(2);
        System.out.println(Thread.currentThread().getName()+":"+threadUser.get().getName());
    }
}

程序執行結果

mainparent
Thread-0:child
main:child

從結果得知父子線程之間的值傳遞確實爲淺複製。子線程對變量的操作,會影響父線程。
如果你想改變此默認行爲,可重寫InheritableThreadLocal的childValue()方法。


註釋1【②】
把hashcode值與數組長度做與運算,這正解釋了數組長度爲什麼要取2的整數冪,因爲這樣數組長度減一正好相當於一個低位掩碼。“與”操作的結果就是散列值的高位全部歸零,只保留低位值,【這樣取得數組下標總是在0~數組長度-1的區間內,因此這樣導致的後果,難免會帶來哈希碰撞。後面再講ThreadLocal是如何處理碰撞的】用來做數組下標訪問。以初始長度16爲例,16-1=15。2進製表示是 00001111。和某hashcode值【假如AtomicIntege一直遞增到100,暫時不考慮負載因子、擴容】做“與”操作如下,結果就是截取了最低的四位值。

      01100100
    & 00001111
---------------------
      00000100  //高位全部歸零,只保留末四位,最終結果爲十進制4

註釋2
就像圖一描述的那樣,每個線程內部都獨立維護着一個ThreadLocalMap對象。ThreadLocal對象作爲key,私有對象作爲value。也就是說只要是關聯了此ThreadLocal對象的線程都有一個指向此ThreadLocal對象的引用。如下圖
這裏寫圖片描述
假設如果某一時刻,程序主動清除了ThreadLocal對象,而關聯此對象的線程又是線程池中的核心線程,永遠不會銷燬。此時,線程中對ThreadLocal對象的引用遲遲不能釋放。那麼ThreadLocal對象就不會被GC回收。這樣就會有造成內存泄漏的可能性。那麼ThreadLocalMap是如何解決這個問題的呢?ThreadLocalMap繼承了WeakReference,它把對ThreadLocal引用置爲弱引用。弱引用的好處就在於當程序中沒有此對象的強引用時,當GC執行的時候,JVM就會回收此對象的內存,減少了內存泄漏的可能性。GC執行後,只是ThreadLocalMap內Entry的key(ThreadLocal)被回收,而value所佔用的內存還沒有被釋放。當程序再次調用其它ThreadLocal對象的get()或者set()方法時,就會清除key爲空的Entry對象,此時Entry對象徹底被回收。


註釋3【③】
在開放尋址法中,所有的元素都存放在散列表裏。也就是說每個表項或包含動態集合的一個元素,或包含NIL。當查找某個元素時,要系統地檢查所有的表項,直到找到所需的元素,或者最終表明該元素不在表中,此過程便稱爲探查。插入一個元素,也是要連續地檢查散列表,直到找到一個空槽來放置待插入的關鍵字爲止。檢查的順序不一定是0,1,…,m-1(這種順序下的查找時間爲Θ(n)),而是要依賴待插入的關鍵字。
在下面的僞代碼中,假設散列表T中的元素爲無衛星數據的關鍵字,關鍵字k等同於包含關鍵字k的元素。每個槽包含一個關鍵字,或包含NIL(如果該槽爲空)。
插入:

HASH-INSERT(T,k)
 i=0
 repeat
  j=h(k,i)
  if T[j]==NIL
    T[j]=k
    return j
  else i=i+1
 until i==m   

查找

HASH-SEARCH(T,k)
 i=0
 repeat
  j=h(k,i)
  if T[j]==k
    return j
  i=i+1
 until T[j]==NIL or i==m
 return NIL   

有三種技術常用來計算開放訊執法中的探查序列:線性探查、二次探查和雙重探查。
ThreadLocalMap採用的便是線性探查。
給定一個普通的散列函數h’:U->{0,1,…,m-1},稱之爲輔助散列函數,線性探查方法採用的散列函數爲:h(k,i)=(h’(k)+i) mod m,i=0,1,…,m-1
給定一個關鍵字k,首先探查槽T[h’(k)],即由輔助散列函數所給出的槽位。再探查槽T[h’(k)+1],以此類推,直至槽T[m-1]。然後,又繞到槽T[0],T[1],…,直到最後探查到槽T[h’(k)-1]。在線性探查方法中,初始探查位置決定了整個序列,故只有m中不同的探查序列。線性探查法較容易實現,但它存在着一個問題,稱爲一次羣集。隨着連續被佔用的槽不斷增加,平均查找時間也隨之不斷增加。羣集現象很容易出現,這是因爲當一個空槽前有i個滿的槽時,該空槽爲下一個將被佔用的概率是(i+1)/m。連續被佔用的空槽就會變得越來越長,因而平均查找時間也會越來越大。


【參考】
①《算法導論》數據結構11.3.2乘法散列法
②關於hashMap的一些按位與計算的問題https://www.zhihu.com/question/28562088
③《算法導論》數據結構11.4開放尋址法

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