1. 程式人生 > >ThreadLocal深入理解與記憶體洩露分析

ThreadLocal深入理解與記憶體洩露分析

ThreadLocal的介面方法

    public T get() { }  
    public void set(T value) { }  
    public void remove() { }  
    protected T initialValue() { }  

get()用來獲取當前執行緒中變數的副本(儲存在ThreadLocal中)。

set()用來設定當前執行緒中變數的副本。

remove()用來移除當前執行緒中變數的副本。

initialValue()是一個protected方法,用來給ThreadLocal變數提供初始值,每個執行緒都會獲取這個初始值的一個副本。


使用示例

public class ThreadLocalTest {  
        //建立一個Integer型的執行緒本地變數  
        public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {  
            @Override  
            protected Integer initialValue() {  
                return 0;  
            }  
        };  
        //計數  
        static class Counter implements Runnable{  
            @Override  
            public void run() {  
                //獲取當前執行緒的本地變數,然後累加100次  
                int num = local.get();  
                for (int i = 0; i < 100; i++) {  
                    num++;  
                }  
                //重新設定累加後的本地變數  
                local.set(num);  
                System.out.println(Thread.currentThread().getName() + " : "+ local.get());  
            }  
        }  
        public static void main(String[] args) throws InterruptedException {  
            Thread[] threads = new Thread[5];  
            for (int i = 0; i < 5; i++) {          
                threads[i] = new Thread(new Counter() ,"CounterThread-[" + i+"]");  
                threads[i].start();  
            }   
        }  
    }  
輸出:

CounterThread-[2] : 100
CounterThread-[0] : 100
CounterThread-[3] : 100
CounterThread-[1] : 100
CounterThread-[4] : 100

對initialValue函式的正確理解

public class ThreadLocalMisunderstand {  
  
    static class Index {  
        private int num;   
        public void increase() {  
            num++;  
        }  
        public int getValue() {  
            return num;  
        }  
    }  
    private static Index num=new Index();  
    //建立一個Index型的執行緒本地變數  
    public static final ThreadLocal<Index> local = new ThreadLocal<Index>() {  
        @Override  
        protected Index initialValue() {  
            return num;  
        }  
    };  
    //計數  
    static class Counter implements Runnable{  
        @Override  
        public void run() {  
            //獲取當前執行緒的本地變數,然後累加10000次  
            Index num = local.get();  
            for (int i = 0; i < 10000; i++) {  
                num.increase();  
            }  
            //重新設定累加後的本地變數  
            local.set(num);  
            System.out.println(Thread.currentThread().getName() + " : "+ local.get().getValue());  
        }  
    }  
    public static void main(String[] args) throws InterruptedException {  
        Thread[] threads = new Thread[5];  
        for (int i = 0; i < 5; i++) {          
            threads[i] = new Thread(new Counter() ,"CounterThread-[" + i+"]");  
        }   
        for (int i = 0; i < 5; i++) {      
            threads[i].start();  
        }  
    }  
}
輸出:

CounterThread-[0] : 12019
CounterThread-[2] : 14548
CounterThread-[1] : 13271
CounterThread-[3] : 34069
CounterThread-[4] : 34069

現在得到的計數不一樣了,並且每次執行的結果也不一樣,說好的執行緒本地變數呢?

之前提到,我們通過覆蓋initialValue函式來給我們的ThreadLocal提供初始值,每個執行緒都會獲取這個初始值的一個副本。而現在我們的初始值是一個定義好的一個物件,num是這個物件的引用。換句話說我們的初始值是一個引用。引用的副本和引用指向的不就是同一個物件嗎?

如果我們想給每一個執行緒都儲存一個Index物件應該怎麼辦呢?那就是建立物件的副本而不是物件引用的副本。

    private static ThreadLocal<Index> local = new ThreadLocal<Index>() {  
        @Override  
        protected Index initialValue() {  
            return new Index(); //注意這裡,新建一個物件  
        }  
    };  

ThreadLocal原始碼分析

儲存結構


public class Thread implements Runnable {
......
    ThreadLocal.ThreadLocalMap threadLocals = null;//一個執行緒對應一個ThreadLocalMap
......
}
public class ThreadLocal<T> {
......
    static class ThreadLocalMap {//靜態內部類
        static class Entry extends WeakReference<ThreadLocal> {//鍵值對
            //Entry是ThreadLocal物件的弱引用,this作為鍵(key)
            /** The value associated with this ThreadLocal. */
            Object value;//ThreadLocal關聯的物件,作為值(value),也就是所謂的執行緒本地變數

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        ......
        private Entry[] table;//用陣列儲存所有Entry,採用線性探測避免衝突
    }
......
}

ThreadLocal原始碼

public class ThreadLocal<T> {
    /**ThreadLocals rely on per-thread linear-probe hash maps attached to each thread 
       (Thread.threadLocals and inheritableThreadLocals).  
       The ThreadLocal objects act as keys, searched via threadLocalHashCode.  */
    //每個執行緒對應一個基於線性探測的雜湊表(ThreadLocalMap型別),通過Thread類的threadLocals屬性關聯。
    //在該雜湊表中,邏輯上key為ThreadLocal,實質上通過threadLocalHashCode屬性作為雜湊值查詢。
    private final int threadLocalHashCode = nextHashCode();

    /**  The next hash code to be given out. Updated atomically. Starts at zero.*/
    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

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

    /**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}.

       <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.
     * @return the initial value for this thread-local
     */
    //初始化執行緒本地變數,注意上面講到的關於該方法的正確理解
    protected T initialValue() {
        return null;
    }

    /**  Creates a thread local variable.*/
    public ThreadLocal() {
    }

    /**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.
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();//獲取當前執行緒物件
        ThreadLocalMap map = getMap(t);//獲取當前執行緒物件關聯的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//以this作為key,查詢執行緒本地變數
            if (e != null)//如果該執行緒本地變數已經存在,返回即可
                return (T)e.value;
        }
        return setInitialValue();//如果該執行緒本地變數不存在,設定初始值並返回
    }

    /**Variant of set() to establish initialValue. 
       Used instead of set() in case user has overridden the set() method.  
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();//獲取執行緒本地變數的初始值
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)//如果當前執行緒關聯的ThreadLocalMap已經存在,將執行緒本地變數插入雜湊表
            map.set(this, value);
        else
            createMap(t, value);//否則,建立新的ThreadLocalMap並將<this,value>組成的鍵值對加入到ThreadLocalMap中
        return value;
    }

    /**Sets the current thread's copy of this thread-local variableto 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.
     *
     * @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();
        ThreadLocalMap map = getMap(t);//獲取當前執行緒的ThreadLocalMap
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /**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 <tt>initialValue</tt> method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {//從當前執行緒的ThreadLocalMap中移除執行緒本地變數
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

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

    /**Create the map associated with a ThreadLocal. Overridden in InheritableThreadLocal.*/
    //建立ThreadLocalMap後與當前執行緒關聯,並將執行緒本地變數插入ThreadLocalMap
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    //其他
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }

    static class ThreadLocalMap {//基於線性探測解決衝突的雜湊對映表
    ......
    }
......
}

記憶體洩露與WeakReference

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

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
對於鍵值對Entry,key為ThreadLocal例項,value為執行緒本地變數。不難發現,Entry繼承自WeakReference<ThreadLocal>。WeakReference就是所謂的弱引用,也就是說Key是一個弱引用(引用ThreadLocal例項)。
關於強引用、弱引用,參看:http://blog.csdn.net/sunxianghuang/article/details/52267282
public class Test {
    public static class MyThreadLocal extends ThreadLocal {
        private byte[] a = new byte[1024*1024*1];
        
        @Override
        public void finalize() {
            System.out.println("My threadlocal 1 MB finalized.");
        }
    }
    
    public static class My50MB {//佔用記憶體的大物件
        private byte[] a = new byte[1024*1024*50];
        
        @Override
        public void finalize() {
            System.out.println("My 50 MB finalized.");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                ThreadLocal tl = new MyThreadLocal();
                tl.set(new My50MB());
                
                tl=null;//斷開ThreadLocal的強引用
                System.out.println("Full GC 1");
                System.gc();
                
            }
            
        }).start();
        System.out.println("Full GC 2");
        System.gc();
        Thread.sleep(1000);
        System.out.println("Full GC 3");
        System.gc();
        Thread.sleep(1000);
        System.out.println("Full GC 4");
        System.gc();
        Thread.sleep(1000);

    }
}
輸出:

Full GC 2
Full GC 1
My threadlocal 1 MB finalized.
Full GC 3
My 50 MB finalized.
Full GC 4

從輸出可以看出,一旦threadLocal的強引用斷開,key的記憶體就可以得到釋放。只有當執行緒結束後,value的記憶體才釋放。

每個thread中都存在一個map, map的型別是ThreadLocal.ThreadLocalMap。Map中的key為一個threadlocal例項。這個Map的確使用了弱引用,不過弱引用只是針對key。每個key都弱引用指向threadlocal。當把threadlocal例項置為null以後,沒有任何強引用指threadlocal例項,所以threadlocal將會被gc回收。但是,我們的value卻不能回收,因為存在一條從current thread連線過來的強引用。

只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收.

注: 實線代表強引用,虛線代表弱引用.


所以得出一個結論就是隻要這個執行緒物件被gc回收,就不會出現記憶體洩露。但是value在threadLocal設為null執行緒結束這段時間不會被回收,就發生了我們認為的“記憶體洩露”。

因此,最要命的是執行緒物件不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用執行緒池的時候,執行緒結束是不會銷燬的,會再次使用的,就可能出現記憶體洩露。  
為了最小化記憶體洩露的可能性和影響,在ThreadLocal的get,set的時候,遇到key為null的entry就會清除對應的value。

所以最怕的情況就是,threadLocal物件設null了,開始發生“記憶體洩露”,然後使用執行緒池,這個執行緒結束,執行緒放回執行緒池中不銷燬,這個執行緒一直不被使用,或者分配使用了又不再呼叫get,set方法,或者get,set方法呼叫時依然沒有遇到key為null的entry,那麼這個期間就會發生真正的記憶體洩露。

使用ThreadLocal需要注意,每次執行完畢後,要使用remove()方法來清空物件,否則 ThreadLocal 存放大物件後,可能會OMM。

為什麼使用弱引用

To help deal with very large and long-lived usages, the hash table entries use  WeakReferences for keys.

應用例項

Hibernate中使用Threadlocal實現執行緒相關的Session

參考:

JDK 1.7原始碼

http://my.oschina.net/xpbug/blog/113444