1. 程式人生 > >ThreadLocal 線程本地變量 及 源碼分析

ThreadLocal 線程本地變量 及 源碼分析

pow initial eat ner 外部 哈希表 reading lmap 合並

■ ThreadLocal 定義

  • ThreadLocal通過為每個線程提供一個獨立的變量副本解決了變量並發訪問的沖突問題
  • 當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本
  • 在ThreadLocal類中有一個Map,用於存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應線程的變量副本
  • ThreadLocal 自我認識:可以把 "賓館" 比作應用程序,每個房間為 "子線程",ThreadLocal 就是為每個 "房間" 提供服務的人員,各自不相互沖突並保持獨立
  • JDK1.5引入泛型後,ThreadLocal告別Object時代進入泛型時代
  • 存儲線程私有變量的一個容器
  • ThreadLocal不是為了解決多線程訪問共享變量,而是為每個線程創建一個單獨的變量副本,提供了保持對象的方法和避免參數傳遞的復雜性

■ ThreadLocal 數據結構

 1. 類定義

public class ThreadLocal<T>

2. 重要的內部元素

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;

3. 構造器

/** ThreadLocal只提供了一個空的默認構造器,夠純粹 **/
public ThreadLocal() {}

■ ThreadLocal 重要方法

 1. set() 方法

/**
  * Sets the current thread‘s copy of this thread-local variable to the specified value. 
  * Most subclasses will have no need to override this method,relying solely on the
  * {@link #initialValue} method to set the values of thread-locals.
  *     設置當前線程在當前ThreadLocal中的線程局部變量的值
  *     其子類無須重寫該方法,只要重寫initialValue方法設置初始默認值即可
  * @param value the value to be stored in the current thread‘s copy of
  *        this thread-local. 將當前值拷貝成當前線程的局部變量
  */
public void set(T value) {
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取當前線程持有的Map
    ThreadLocalMap map = getMap(t);
    //createMap or set directly
    if (map != null)
        //註意 key為this,指的就是當前調用set方法的ThreadLocal對象本身
        map.set(this, value);
    else
        //根據當前線程初始化它的ThreadLocalMap並設置值
        createMap(t, value);
}

 2. get() 方法

/**
  * Returns the value in the current thread‘s copy of this thread-local variable. 
  * If the variable has no value for the current thread,it is first initialized to 
  * the value returned by an invocation of the {@link #initialValue} method.
  *     返回當前線程在當前ThreadLocak中所對應的線程局部變量
  *     若當前值不存在,則返回initialValue方法設置的初始默認值
  * @return the current thread‘s value of this thread-local
  */
public T get() {
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取當前線程持有的Map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    //Map為空或值為空,返回默認值
    return setInitialValue();
}

 3. remove() 方法

/**
  * Removes the current thread‘s value for this thread-local variable.  
  * If this thread-local variable is subsequently {@linkplain #get read} 
  * by the current thread, its value will be reinitialized by invoking its
  * {@link #initialValue} method, unless its value is {@linkplain #set set}
  * by the current thread in the interim.  This may result in multiple invocations 
  * of the {@code initialValue} method in the current thread.
  *     移除當前線程在當前ThreadLocal中對應的私有變量
  *     當該變量之後被當前線程讀取(get),該值會重新被initialValue方法初始化除非這期間被set
  *     這將會導致initialValue方法會被當前線程多次調用
  * @since 1.5 該方法是JDK1.5新增方法
  */
public void remove() {
    //獲取當前線程持有的ThreadLocalMap,由此可見get和set中的相關代碼也應該合並為一行
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        //註意是從ThreadLocalMap移除的當前ThreadLocal對象(即ThreadLocalMap的key)
        m.remove(this);
}

 4. getMap, createMap 方法

/**
  * Get the map associated with a ThreadLocal. Overridden in
  * InheritableThreadLocal.
  *
  * @param  t the current thread
  * @return the map
  */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}


/**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
 }

 5. nextHashCode 方法

/**
     * Returns the next hash code.
   */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

6. initialValue 方法

/**
  * Returns the current thread‘s "initial value" for this thread-local variable. 
  * This method will be invoked the first time a thread accesses the variable
  * with the {@link #get} method, unless the thread previously invoked the {@link #set}
  * method, in which case the <tt>initialValue</tt> method will not be invoked for the thread.
  * Normally, this method is invoked at most once per thread, but it may be invoked again
  * in case of subsequent invocations of {@link #remove} followed by {@link #get}.
  *     返回當前線程在當前ThreadLocal中的初始默認值
  *     第一次get操作會調用該方法,除非之前已經調用了set方法(即已有值)
  *     一般情況下該方法只會被執行一次,但有可能出現多次,比如:
  *         調用remove方法之後調用了get方法
  * <p>This implementation simply returns <tt>null</tt>; if the programmer desires
  * thread-local variables to have an initial value other than <tt>null</tt>, 
  * <tt>ThreadLocal</tt> must be subclassed, and this method overridden.
  * Typically, an anonymous inner class will be used.
  *     該方法默認返回null,可以重寫該方法(比如繼承或實現一個匿名類)
  * @return the initial value for this thread-local
  */
protected T initialValue() {
    return null;
}
---------------
/** 比如 自定義一個String類型的匿名ThreadLocal**/
ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>(){
    @Override
    protected String initialValue() {
        return "I am roman";
    }
};

■ ThreadLocalMap - 線程隔離的秘密

  • ThreadLocalMap是一個專門為線程本地變量設計的一個特殊的哈希表
  • ThreadLocalMap的key為ThreadLocal,value即為要保存的變量的值
  • 每個線程都有一個私有的ThreadLocalMap對象,其可以存放多個不同ThreadLocal作為key的鍵值對
  • ThreadLocalMap采用的是開地址法而不是鏈表來解決沖突,並要求容量必須是2次冪

1. 類定義

static class ThreadLocalMap

2. Entry

/**
  * The entries in this hash map extend WeakReference, using
  * its main ref field as the key (which is always a
  * ThreadLocal object).  Note that null keys (i.e. entry.get()
  * == null) mean that the key is no longer referenced, so the
  * entry can be expunged from table.  Such entries are referred to
  * as "stale entries" in the code that follows.
  *     它使用主要的引用域作為自身的key(即ThreadLocal對象)
  *     由於Entry繼承自WeakReference,而ThreadLocal被WeakReference封裝
  *     !!重點:因此Entry的Key才是弱引用(而不是Entry)!!(筆者在內存泄露會進一步闡述)
  *     當調用get方法返回null時,這意味著該key不再被引用,因此該entry將會從數組中移除
  *     弱引用:當JVM在GC時如果發現弱引用就會立即回收
  *     比較有意思的是Entry並沒有使用HashMap.Entry的鏈表結構
  *     感興趣的讀者可先思考ThreadLocalMap是如何處理hash沖突的問題(後面就講解)
  */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //當ThreadLocal的外部強引用被回收時,ThreadLocalMap的key會變成null
    //註意key是個ThreaLocal對象,但因為key被WeakReference封裝,因此才具有弱引用特性
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

3. 重要的內部元素

 /**
 * The initial capacity -- MUST be a power of two.
 *  容量必須2次冪,服務於Hash算法
 */
private static final int INITIAL_CAPACITY = 16;
/**
 * The table, resized as necessary. table.length MUST always be a power of two.
 *  底層實現還是一個Entry數組
 */
private Entry[] table;
/**
  * The number of entries in the table.
  * 數組已有元素數量
  */
private int size = 0;
/**
  * The next size value at which to resize.
  * 閾值,默認為0
  */
private int threshold; // Default to 0

4. 構造器

/**
  * Construct a new map initially containing (firstKey, firstValue).
  * ThreadLocalMaps are constructed lazily, so we only create
  * one when we have at least one entry to put in it.
  *     默認構造器,包含一個鍵值對:一個ThreadLocal類型的key,一個任意類型的value
  *     createMap方法會直接使用該構造器一次性完成ThreadLocalMap的實例化和鍵值對的存儲
  */
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    //計算數組下標 跟HashMap的 index = key.hashCode() & (cap -1) 保持一致(即取模運算優化版) 
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //在數組指定下標處填充數組
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);//默認閾值是 32/3 約等於 10.6667
}
/**
 * Set the resize threshold to maintain at worst a 2/3 load factor.
 *      取len的三分之二,而不是HashMap的0.75
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

5. ThreadLocalMap 一些重要方法

  1) set 方法

/**
  * Set the value associated with key.
  *     存儲鍵值對,比較有趣的是Entry並不是鏈表,這意味著ThreadLocalMap底層只是數組
  *     其解決沖突(或者說散列優化)的關鍵在於神奇的0x61c88647
  *     若遇到過期槽,就占用該過期槽(會涉及位移和槽清除操作)
  *     當清理成功同時到達閾值,需要擴容
  * @param key the thread local object
  * @param value the value to be set
  */
private void set(ThreadLocal key, Object value) {
    Entry[] tab = table;
    int len = tab.length;//數組容量
    //計算數組下標 跟HashMap的 index = key.hashCode() & (cap -1) 保持一致(即取模運算優化版) 
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();
        //若key已存在,替換值即可
        if (k == key) {
            e.value = value;
            return;
        }
        //若當前槽為過期槽,就清除和占用該過期槽
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
        //否則繼續往後 直到找到key相等或第一個過期槽為止
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //當清理成功同時到達閾值,需要擴容
    //cleanSomeSlots要處理的量是已有元素數量
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
/**
  * Increment i modulo len. 不超過長度就自增1
  */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

2) remove 方法

/**
  * Remove the entry for key.
  *     當找到該元素的時候,主要做了兩個清洗操作
  *         1.將key(ThreadLocal)設置為null
  *         2.當前槽變成過期槽,因此要清除當前槽所存儲的Entry元素(主要是避免內存泄露)
  */
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();//會將key設為null -> this.referent = null
            expungeStaleEntry(i);//清除過期元素
            return;
        }
    }
}

■ 碰撞解決與神奇的0x61c88647

  • 機智的讀者肯定發現ThreadLocalMap並沒有使用鏈表或紅黑樹去解決hash沖突的問題,而僅僅只是使用了數組來維護整個哈希表,那麽重中之重的散列性要如何保證就是一個很大的考驗
  • ThreadLocalMap 通過結合三個巧妙的設計去解決這個問題:
     1. Entry的key設計成弱引用,因此key隨時可能被GC(也就是失效快),盡量多的面對空槽
    2. (單個ThreadLocal時)當遇到碰撞時,通過線性探測的開放地址法解決沖突問題
    3. (多個ThreadLocal時)引入了神奇的0x61c88647,增強其的散列性,大大減少碰撞幾率
  • 之所以不用累加而用該值,筆者認為可能跟其找最近的空槽有關(跳躍查找比自增1查找用來找空槽可能更有效一些,因為有了更多可選擇的空間spreading out),同時也跟其良好的散列性有關
/**
 * The difference between successively generated hash codes - turns
 * implicit sequential thread-local IDs into near-optimally spread
 * multiplicative hash values for power-of-two-sized tables.
 *  為了讓哈希碼能均勻的分布在2的N次方的數組裏
 */
private static final int HASH_INCREMENT = 0x61c88647;
/**
 * Returns the next hash code.
 *  每個ThreadLocal的hashCode每次累加HASH_INCREMENT
 */
private static int nextHashCode() {
    //the previous id + our magic number
    return nextHashCode.getAndAdd(HASH_INCREMENT); 
}

■ ThreadLocal 的實現機制

  • 每個線程都擁有一個ThreadLocalMap對象,即 ThreadLocal.ThreadLocalMap threadLocals = null
  • 每一個ThreadLocal對象有一個創建時生成唯一的HashCode,即 nextHashCode(),通過取模確定所在槽下標位置
  • 訪問一個ThreadLocal變量的值,即是查找ThreadLocalMap中對應鍵值對,即key為該ThreadLocal的鍵值對
  • 由於一個ThreadLocalMap可以擁有很多個ThreadLocal,推導可得一個線程可擁有多個ThreadLocal(或者說擁有多個不同ThreadLocal作為key的鍵值對)
//可以定義多個ThreadLocal,每個線程都擁有自己私有的各種泛型的ThreadLocal
//比如線程A可同時擁有以下三個ThreadLocal對象作為key
ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();
ThreadLocal<Object> objectThreadLocal = new ThreadLocal<Object>();
ThreadLocal<Integer> intThreadLocal = new ThreadLocal<Integer>();

PS:

***** 後續會和大家討論一些內存泄露與ThreadLocal 的實際應用問題,請多指教!! ************

ThreadLocal 線程本地變量 及 源碼分析