完全理解ThreadLocal

完全理解ThreadLocal

參考:http://www.iteye.com/topic/103804
http://www.iteye.com/topic/777716java

源碼分析

  爲了解釋ThreadLocal類的工做原理,必須同時介紹與其工做甚密的其餘幾個類多線程

  • ThreadLocalMap(內部類)
  • Thread

  首先,在Thread類中有一行:併發

/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

  其中ThreadLocalMap類的定義是在ThreadLocal類中,真正的引用倒是在Thread類中。同時,ThreadLocalMap中用於存儲數據的entry定義:函數

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

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

  從中咱們能夠發現這個Map的key是ThreadLocal類的實例對象,value爲用戶的值,並非網上大多數的例子key是線程的名字或者標識。ThreadLocal的set和get方法代碼:源碼分析

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

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

其中的getMap方法:this

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

給當前Thread類對象初始化ThreadlocalMap屬性:線程

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

  到這裏,咱們就能夠理解ThreadLocal到底是如何工做的了設計

  1. Thread類中有一個成員變量屬於ThreadLocalMap類(一個定義在ThreadLocal類中的內部類),它是一個Map,他的key是ThreadLocal實例對象。
  2. 當爲ThreadLocal類的對象set值時,首先得到當前線程的ThreadLocalMap類屬性,而後以ThreadLocal類的對象爲key,設定value。get值時則相似。
  3. ThreadLocal變量的活動範圍爲某線程,是該線程「專有的,獨自霸佔」的,對該變量的全部操做均由該線程完成!也就是說,ThreadLocal 不是用來解決共享對象的多線程訪問的競爭問題的,由於ThreadLocal.set() 到線程中的對象是該線程本身使用的對象,其餘線程是不須要訪問的,也訪問不到的。當線程終止後,這些值會做爲垃圾回收。
  4. 由ThreadLocal的工做原理決定了:每一個線程獨自擁有一個變量,並不是是共享的,下面給出一個例子:
public class Son implements Cloneable{
    public static void main(String[] args){
        Son p=new Son();
        System.out.println(p);
        Thread t = new Thread(new Runnable(){  
            public void run(){
                ThreadLocal<Son> threadLocal = new ThreadLocal<>();
                System.out.println(threadLocal);
                threadLocal.set(p);
                System.out.println(threadLocal.get());
                threadLocal.remove();
                try {
                    threadLocal.set((Son) p.clone());
                    System.out.println(threadLocal.get());
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadLocal);
            }}); 
        t.start();
    }
}

輸出:code

Son@7852e922
java.lang.ThreadLocal@3ffc8195
Son@7852e922
Son@313b781a
java.lang.ThreadLocal@3ffc8195

也就是若是把一個共享的對象直接保存到ThreadLocal中,那麼多個線程的ThreadLocal.get()取得的仍是這個共享對象自己,仍是有併發訪問問題。 因此要在保存到ThreadLocal以前,經過克隆或者new來建立新的對象,而後再進行保存。
  ThreadLocal的做用是提供線程內的局部變量,這種變量在線程的生命週期內起做用。做用:提供一個線程內公共變量(好比本次請求的用戶信息),減小同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度,或者爲線程提供一個私有的變量副本,這樣每個線程均可以隨意修改本身的變量副本,而不會對其餘線程產生影響。對象

如何實現一個線程多個ThreadLocal對象,每個ThreadLocal對象是如何區分的呢?
查看源碼,能夠看到:

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
}

  對於每個ThreadLocal對象,都有一個final修飾的int型的threadLocalHashCode不可變屬性,對於基本數據類型,能夠認爲它在初始化後就不能夠進行修改,因此能夠惟一肯定一個ThreadLocal對象。
  可是如何保證兩個同時實例化的ThreadLocal對象有不一樣的threadLocalHashCode屬性:在ThreadLocal類中,還包含了一個static修飾的AtomicInteger([əˈtɒmɪk]提供原子操做的Integer類)成員變量(即類變量)和一個static final修飾的常量(做爲兩個相鄰nextHashCode的差值)。因爲nextHashCode是類變量,因此每一次調用ThreadLocal類均可以保證nextHashCode被更新到新的值,而且下一次調用ThreadLocal類這個被更新的值仍然可用,同時AtomicInteger保證了nextHashCode自增的原子性。

爲何不直接用線程id來做爲ThreadLocalMap的key?
  這一點很容易理解,由於直接用線程id來做爲ThreadLocalMap的key,沒法區分放入ThreadLocalMap中的多個value。好比咱們放入了兩個字符串,你如何知道我要取出來的是哪個字符串呢?
  而使用ThreadLocal做爲key就不同了,因爲每個ThreadLocal對象均可以由threadLocalHashCode屬性惟一區分或者說每個ThreadLocal對象均可以由這個對象的名字惟一區分(下面的例子),因此能夠用不一樣的ThreadLocal做爲key,區分不一樣的value,方便存取。

public class Son implements Cloneable{
    public static void main(String[] args){
        Thread t = new Thread(new Runnable(){  
            public void run(){
                ThreadLocal<Son> threadLocal1 = new ThreadLocal<>();
                threadLocal1.set(new Son());
                System.out.println(threadLocal1.get());
                ThreadLocal<Son> threadLocal2 = new ThreadLocal<>();
                threadLocal2.set(new Son());
                System.out.println(threadLocal2.get());
            }}); 
        t.start();
    }
}

ThreadLocal的內存泄露問題
  根據上面Entry方法的源碼,咱們知道ThreadLocalMap是使用ThreadLocal的弱引用做爲Key的。下圖是本文介紹到的一些對象之間的引用關係圖,實線表示強引用,虛線表示弱引用:

  如上圖,ThreadLocalMap使用ThreadLocal的弱引用做爲key,若是一個ThreadLocal沒有外部強引用引用他,那麼系統gc的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,若是當前線程再遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠沒法回收,形成內存泄露。
  
  ThreadLocalMap設計時的對上面問題的對策:
ThreadLocalMap的getEntry函數的流程大概爲:

  1. 首先從ThreadLocal的直接索引位置(經過ThreadLocal.threadLocalHashCode & (table.length-1)運算獲得)獲取Entry e,若是e不爲null而且key相同則返回e;
  2. 若是e爲null或者key不一致則向下一個位置查詢,若是下一個位置的key和當前須要查詢的key相等,則返回對應的Entry。不然,若是key值爲null,則擦除該位置的Entry,並繼續向下一個位置查詢。在這個過程當中遇到的key爲null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,天然會被回收。仔細研究代碼能夠發現,set操做也有相似的思想,將key爲null的這些Entry都刪除,防止內存泄露。
      可是光這樣仍是不夠的,上面的設計思路依賴一個前提條件:要調用ThreadLocalMap的getEntry函數或者set函數。這固然是不可能任何狀況都成立的,因此不少狀況下須要使用者手動調用ThreadLocal的remove函數,手動刪除再也不須要的ThreadLocal,防止內存泄露。因此JDK建議將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,因爲一直存在ThreadLocal的強引用,因此ThreadLocal也就不會被回收,也就能保證任什麼時候候都能根據ThreadLocal的弱引用訪問到Entry的value值,而後remove它,防止內存泄露。

關於ThreadLocalMap內部類的簡單介紹   初始容量16,負載因子2/3,解決衝突的方法是再hash法,也就是:在當前hash的基礎上再自增一個常量。

相關文章
相關標籤/搜索