1. 程式人生 > >ThreadLocal深入理解與內存泄露分析

ThreadLocal深入理解與內存泄露分析

fonts statistic rac jdk 占用內存 得出 銷毀 prev other

ThreadLocal

當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本。所以每個線程都能夠獨立地改變自己的副本。而不會影響其他線程所相應的副本。從線程的角度看。目標變量就象是線程的本地變量。這也是類名中“Local”所要表達的意思。

能夠理解為例如以下三點:
1、每一個線程都有自己的局部變量
每一個線程都有一個獨立於其它線程的上下文來保存這個變量,一個線程的本地變量對其它線程是不可見的。
2、獨立於變量的初始化副本
ThreadLocal能夠給一個初始值。而每一個線程都會獲得這個初始化值的一個副本

,這樣才幹保證不同的線程都有一份拷貝。
3、狀態與某一個線程相關聯
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

ThreadLocal深入理解與內存泄露分析