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


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

Threadlocal 傳遞參數(百度二面)

總結:
  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方法,那么這個期間就會發生真正的內存泄露。 

應用場景

最常見的ThreadLocal使用場景為 用來解決 數據庫連接、Session管理等。如

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是什么?有什么用?
     2、ThreadLocal源碼簡要總結?
     3、ThreadLocal為什么會導致內存泄漏?

二、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

 

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

三、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的東西。
 

四、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從源碼分析總結到內存泄漏


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM