ThreadLocal 定義,以及是否可能引發的內存泄露(threadlocalMap的Key是弱引用,用線程池有可能泄露)

ThreadLocal 也能夠跟蹤一個請求,從接收請求,處理請求,到返回請求,只要線程不銷燬,就能夠在線程的任何地方,調用這個參數,這是百度二面的題目,參考:

Threadlocal 傳遞參數(百度二面)html

總結:
  1. JVM利用設置ThreadLocalMap的Key爲弱引用,來避免內存泄露。
  2. JVM利用調用remove、get、set方法的時候,回收弱引用。
  3. 當ThreadLocal存儲不少Key爲null的Entry的時候,而再也不去調用remove、get、set方法,那麼將致使內存泄漏。
  4. 當使用static ThreadLocal的時候,延長ThreadLocal的生命週期,那也可能致使內存泄漏。由於,static變量在類未加載的時候,它就已經加載,當線程結束的時候,static變量不必定會回收。那麼,比起普通成員變量使用的時候才加載,static的生命週期加長將更容易致使內存泄漏危機。http://www.importnew.com/22039.html

 

那麼如何有效的避免呢?數據庫

事實上,在ThreadLocalMap中的set/getEntry方法中,會對key爲null(也便是ThreadLocal爲null)進行判斷,若是爲null的話,那麼是會對value置爲null的。咱們也能夠經過調用ThreadLocal的remove方法進行釋放!編程

threadlocal裏面使用了一個存在弱引用的map,當釋放掉threadlocal的強引用之後,map裏面的value卻沒有被回收.而這塊value永遠不會被訪問到了. 因此存在着內存泄露. 最好的作法是將調用threadlocal的remove方法.數組

  在threadlocal的生命週期中,都存在這些引用. 看下圖: 實線表明強引用,虛線表明弱引用.緩存

  

  每一個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回收,就不會出現內存泄露,但在threadLocal設爲null和線程結束這段時間不會被回收的,就發生了咱們認爲的內存泄露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是線程對象不被回收的狀況,這就發生了真正意義上的內存泄露。好比使用線程池的時候,線程結束是不會銷燬的,會再次使用的。就可能出現內存泄露。  網絡

  PS.Java爲了最小化減小內存泄露的可能性和影響,在ThreadLocal的get,set的時候都會清除線程Map裏全部key爲null的value。因此最怕的狀況就是,threadLocal對象設null了,開始發生「內存泄露」,而後使用線程池,這個線程結束,線程放回線程池中不銷燬,這個線程一直不被使用,或者分配使用了又再也不調用get,set方法,那麼這個期間就會發生真正的內存泄露。 session

應用場景

最多見的ThreadLocal使用場景爲 用來解決 數據庫鏈接、Session管理等。如mybatis

private static ThreadLocal < Connection > connectionHolder = new ThreadLocal < Connection > () {
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};

public static Connection getConnection() {
    return connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
} 

1、目錄

     一、ThreadLocal是什麼?有什麼用?
     二、ThreadLocal源碼簡要總結?
     三、ThreadLocal爲何會致使內存泄漏?

2、ThreadLocal是什麼?有什麼用?

引入話題:在併發條件下,如何正確得到共享數據?舉例:假設有多個用戶須要獲取用戶信息,一個線程對應一個用戶。在mybatis中,session用於操做數據庫,那麼設置、獲取操做分別是session.set()、session.get(),如何保證每一個線程都能正確操做達到想要的結果?
/**
 * 回顧synchronized在多線程共享線程的問題
 * @author qiuyongAaron
 */
public class ThreadLocalOne {
     volatile Person person=new Person();
 
     public  synchronized String setAndGet(String name){
          //System.out.print(Thread.currentThread().getName()+":");
           person.name=name;
           //模擬網絡延遲
           try {
                TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
                e.printStackTrace();
           }
           return person.name;
     }
 
     public static void main(String[] args) {
           ThreadLocalOne  threadLocal=new ThreadLocalOne();
           new Thread(()->System.out.println(threadLocal.setAndGet("arron")),"t1").start();
           new Thread(()->System.out.println(threadLocal.setAndGet("tony")),"t2").start();
     }
}
 
class Person{
     String name="tom";
     public Person(String name) {
           this.name=name;
     }
 
     public Person(){}
}
 
運行結果:
無synchronized:
t1:tony
t2:tony
 
有synchronized:
t1:arron
t2:tony

 

步驟分析:
  1. 無synchronized的時候,由於非原子操做,顯然不是預想結果,可參考我關於synchronized的討論。
  2. 如今,咱們的需求是:每一個線程獨立的設置獲取person信息,不被線程打擾。
  3. 由於,person是共享數據,用同步互斥鎖synchronized,當一個線程訪問共享數據的時候,其餘線程堵塞,再也不多餘贅述。
 
經過舉例問題,可能你們又會很疑惑?
mybatis、hibernate是如何實現的呢?
synchronized不會很消耗資源,當成千上萬個操做的時候,承受併發不說,數據返回延遲如何確保用戶體驗?
 
ThreadLocal是什麼?有什麼用?
/**
 * 談談ThreadLocal的做用
 * @author qiuyongAaron
 */
public class ThreadLocalThree {
     ThreadLocal<Person> threadLocal=new ThreadLocal<Person>();
     public String setAndGet(String name){
           threadLocal.set(new Person(name));
           try {
                TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
                e.printStackTrace();
           }
           return threadLocal.get().name;
     }
 
     public static void main(String[] args) {
           ThreadLocalThree  threadLocal=new ThreadLocalThree();
           new Thread(()->System.out.println("t1:"+threadLocal.setAndGet("arron")),"t1").start();
           new Thread(()->System.out.println("t2:"+threadLocal.setAndGet("tony")),"t2").start();
     }
}
運行結果:
t1:arron
t2:tony

 

 
分析:
一、根據預期結果,那ThreadLocal究竟是什麼?
回顧Java內存模型:
  
      在虛擬機中,堆內存用於存儲共享數據(實例對象),堆內存也就是這裏說的主內存。
     每一個線程將會在堆內存中開闢一塊空間叫作線程的工做內存,附帶一塊緩存區用於存儲共享數據副本。那麼,共享數據在堆內存當中,線程通訊就是經過主內存爲中介,線程在本地內存讀而且操做完共享變量操做完畢之後,把值寫入主內存。
  1. ThreadLocal被稱爲線程局部變量,說白了,他就是線程工做內存的一小塊內存,用於存儲數據。
  2. 那麼,ThreadLocal.set()、ThreadLocal.get()方法,就至關於把數據存儲於線程本地,取也是在本地內存讀取就不會像synchronized須要頻繁的修改主內存的數據,再把數據複製到工做內存,也大大提升訪問效率
 
二、ThreadLocal到底有什麼用?
  1. 回到最開始的舉例,也就等價於mabatis、hibernate爲何要使用threadlocal來存儲session?
  2. 做用一:由於線程間的數據交互是經過工做內存與主存的頻繁讀寫完成通訊,然而存儲於線程本地內存,提升訪問效率,避免線程阻塞形成cpu吞吐率降低
  3. 做用二:在多線程中,每個線程都須要維護session,輕易完成對線程獨享資源的操做
 
總結:
     Threadlocal是什麼?在堆內存中,每一個線程對應一塊工做內存,threadlocal就是工做內存的一小塊內存。
     Threadlocal有什麼用?threadlocal用於存取線程獨享數據,提升訪問效率。

3、ThreadLocal源碼簡要總結?

那有同窗可能仍是有點雲裏霧裏,感受仍是沒有吃透?那線程內部如何去保證線程獨享數據呢?
 
在這裏,我只作簡要總結,如有興趣,可參考文章尾部的文章連接。重點看get、set方法。
 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
 }

 

分析:
  1. 一個線程對應一個ThreadLocalMap ,能夠存儲多個ThreadLocal對象。
  2. ThreadLocal對象做爲key、獨享數據做爲value。
  3. ThreadLocalMap可參考HashMap,在ThreadMap裏面存在Entry數組也就是一個Entry一個鍵值對。
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();
    }

 

分析:
  1. 一個線程對應一個ThreadLocalMap,get()就是當前線程獲取本身的ThreadLocalMap。
  2. 線程根據使用那一小塊的threadlocal,根據ThreadLocal對象做爲key,去獲取存儲於ThreadLocalMap中的值。
 
總結:
     回顧一下,咱們在單線程中如何使用HashMap的?hashMap根據數組+鏈表來實現HashMap,一個key對應一個value。那麼,咱們抽象一下,Threadlocal也至關於在多線程中的一種HashMap用法,至關於對ThradLocal的操做也就如單線程操做同樣。
     總之,ThreadLocal就是堆內存的一塊小內存,它用ThreadLocalMap維護ThreadLocal對象做爲key,獨享數據做爲value的東西。
 

4、ThreadLocal爲何會致使內存泄漏?

synchronized是用時間換空間(犧牲時間)、ThreadLocal是用空間換時間(犧牲空間),爲何這麼說?
由於synchronized操做數據,只須要在主存存一個變量便可,就阻塞等共享變量,而ThreadLocal是每一個線程都建立一塊小的堆工做內存。顯然,印證了上面的說法。
 
一個線程對應一塊工做內存,線程能夠存儲多個ThreadLocal。那麼假設,開啓1萬個線程,每一個線程建立1萬個ThreadLocal,也就是每一個線程維護1萬個ThreadLocal小內存空間,並且當線程執行結束之後,假設這些ThreadLocal裏的Entry還不會被回收,那麼將很容易致使堆內存溢出。
 
怎麼辦?難道JVM就沒有提供什麼解決方案嗎?
ThreadLocal固然有想到,因此他們把ThreadLocal裏的Entry設置爲弱引用,當垃圾回收的時候,回收ThreadLocal。
什麼是弱引用?
  1. Key使用強引用:也就是上述說的狀況,引用ThreadLocal的對象被回收了,ThreadLocal的引用ThreadLocalMap的Key爲強引用並無被回收,若是不手動回收的話,ThreadLocal將不會回收那麼將致使內存泄漏。
  2. Key使用弱引用:引用的ThreadLocal的對象被回收了,ThreadLocal的引用ThreadLocalMap的Key爲弱引用,若是內存回收,那麼將ThreadLocalMap的Key將會被回收,ThreadLocal也將被回收。value在ThreadLocalMap調用get、set、remove的時候就會被清除
  3. 比較兩種狀況,咱們能夠發現:因爲ThreadLocalMap的生命週期跟Thread同樣長,若是都沒有手動刪除對應key,都會致使內存泄漏,可是使用弱引用能夠多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除
 
那按你這麼說,既然JVM有保障了,還有什麼內存泄漏可言?
ThreadLocalMap使用ThreadLocal對象做爲弱引用,當垃圾回收的時候,ThreadLocalMap中Key將會被回收,也就是將Key設置爲null的Entry。 若是線程遲遲沒法結束,也就是ThreadLocal對象將一直不會回收,回顧到上面存在不少線程+TheradLocal,那麼也將致使內存泄漏。(內存泄露的重點)
 
其實,在ThreadLocal中,當調用remove、get、set方法的時候,會清除爲null的弱引用,也就是回收ThreadLocal。
  ThreadLocal提供一個線程(Thread)局部變量,訪問到某個變量的每個線程都擁有本身的局部變量。說白了,ThreadLocal就是想在多線程環境下去保證成員變量的安全。 

ThreadLocal提供的方法多線程

 
ThreadLocal API

對於ThreadLocal而言,經常使用的方法,就是get/set/initialValue方法。

咱們先來看一個例子

 
demo

運行結果

 
是你想象中的結果麼?

很顯然,在這裏,並無經過ThreadLocal達到線程隔離的機制,但是ThreadLocal不是保證線程安全的麼?這是什麼鬼?

雖然,ThreadLocal讓訪問某個變量的線程都擁有本身的局部變量,可是若是這個局部變量都指向同一個對象呢?這個時候ThreadLocal就失效了。仔細觀察下圖中的代碼,你會發現,threadLocal在初始化時返回的都是同一個對象a!

 

看一看ThreadLocal源碼

咱們直接看最經常使用的set操做:

 
set

 

 
線程局部變量

 

 
createMap
 

你會看到,set須要首先得到當前線程對象Thread;

而後取出當前線程對象的成員變量ThreadLocalMap;

若是ThreadLocalMap存在,那麼進行KEY/VALUE設置,KEY就是ThreadLocal;

若是ThreadLocalMap沒有,那麼建立一個;

說白了,當前線程中存在一個Map變量,KEY是ThreadLocal,VALUE是你設置的值。

看一下get操做:
 
get

這裏其實揭示了ThreadLocalMap裏面的數據存儲結構,從上面的代碼來看,ThreadLocalMap中存放的就是Entry,Entry的KEY就是ThreadLocal,VALUE就是值。

ThreadLocalMap.Entry:

 
弱引用?
 

在JAVA裏面,存在強引用、弱引用、軟引用、虛引用。這裏主要談一下強引用和弱引用。

強引用,就沒必要說了,相似於:

A a = new A();

B b = new B();

考慮這樣的狀況:

C c = new C(b);

b = null;

考慮下GC的狀況。要知道b被置爲null,那麼是否意味着一段時間後GC工做能夠回收b所分配的內存空間呢?答案是否認的,由於即使b被置爲null,可是c仍然持有對b的引用,並且仍是強引用,因此GC不會回收b原先所分配的空間!既不能回收利用,又不能使用,這就形成了內存泄露

那麼如何處理呢?

能夠c = null;也可使用弱引用!(WeakReference w = new WeakReference(b);)

分析到這裏,咱們能夠獲得:

 
內存結構圖

這裏咱們思考一個問題:ThreadLocal使用到了弱引用,是否意味着不會存在內存泄露呢? 

首先來講,若是把ThreadLocal置爲null,那麼意味着Heap中的ThreadLocal實例不在有強引用指向,只有弱引用存在,所以GC是能夠回收這部分空間的,也就是key是能夠回收的。可是value卻存在一條從Current Thread過來的強引用鏈。所以只有當Current Thread銷燬時,value才能獲得釋放。

所以,只要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設爲null和線程結束這段時間內不會被回收的,就發生了咱們認爲的內存泄露。最要命的是線程對象不被回收的狀況,好比使用線程池的時候,線程結束是不會銷燬的,再次使用的,就可能出現內存泄露。

那麼如何有效的避免呢?

事實上,在ThreadLocalMap中的set/getEntry方法中,會對key爲null(也便是ThreadLocal爲null)進行判斷,若是爲null的話,那麼是會對value置爲null的。咱們也能夠經過調用ThreadLocal的remove方法進行釋放!

 

 
 

參考:ThreadLocal可能引發的內存泄露

參考:對ThreadLocal實現原理的一點思考

參考:併發編程(四):ThreadLocal從源碼分析總結到內存泄漏

相關文章
相關標籤/搜索