天天看點

ThreadLocal原理、源碼、記憶體洩露分析

ThreadLocal

ThreadLocal類和Thread類都位于

java.lang

包下面,關系緊密。Thread類裡面有一個成員變量

也就是說,每個線程都有自己獨立的

ThreadLocalMap

(容器)。

ThreadLocal

就是通過與線程的這個

ThreadLocalMap threadLocals

變量發生互動來實作線程級變量私有和隔離的。

ThreadLocal.set()

的執行邏輯是:

首先拿到目前的Thread對象,然後往目前線程自己的

ThreadLocalMap

容器中塞一個值進去。

ThreadLocal.get()

也是從自己線程的容器中取。

這樣就做到了線程隔離。每個線程通過

ThreadLocal.get()

得到的值,是自己這個線程先前塞到容器中的值(或者得到

null

,如果目前線程之前沒有

set

過值的話)。

測試代碼

public class TestThreadLocal {
    static ThreadLocal<Node> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            //目前線程還未設定過,此處get傳回null
            Node person = threadLocal.get();
            System.out.println("T1->" + person);
            threadLocal.set(new Node());
            //此處get傳回Node{value=0}
            person = threadLocal.get();
            System.out.println("T1 after->" + person);
        },"T1");

        Thread t2 = new Thread(()->{
            //目前線程還未設定過,此處get傳回null
            Node person = threadLocal.get();
            System.out.println("T2 before->" + person);
            TimeUnit.SECONDS.sleep(5);

            //T1已經設定過了,但目前線程還未設定過,此處get仍然傳回null,不同線程間的通路是互相隔離的
            Node person2 = threadLocal.get();
            System.out.println("T2 after->" + person2);
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
    private static class Node {
        public int value = 0;
    }
}
           

ThreadLocalMap

ThreadLocalMap

可以看成一個存儲鍵值對(

key-value

)的容器,容器裡面的每一條記錄是一個

Entry

:

// ThreadLocal.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;
    }
}
           

其中key就是

ThreadLocal

類型的執行個體變量,

Object

類型的value就是每個線程希望獨占一份的目标變量。

每個線程中所持有的這個線程私有的

ThreadLocalMap

容器就是

ThreadLocal

實作線程隔離的關鍵。

有幾個點值得詳細讨論一下:

  1. 同一個線程内部,所有ThreadLocal變量所持有的線程私有變量都是放在這一個ThreadLocalMap容器中的。
  2. 不同于java.util.HashMap采用連結清單法(連結清單+紅黑樹)處理哈希沖突,ThreadLocalMap實作上采用開放尋址法。

處理hash沖突常見的幾種方式:

a 開放尋址法,包括線性尋址(+1,+1,+1,…)、平方尋址(+1,+4,+9,…)

b 連結清單法,将相同hash值的對象通過一個鍊條串在該hash值所對應的槽位上。

c 再哈希

d 建立公共溢出區,凡是發生hash沖突的對象都存放到溢出區。

  1. Entry為什麼做成

    WeakReference

    (弱引用)呢?
我們知道,隻被弱引用所持有的對象,每次GC一旦執行,該對象就會被回收。

其實道理也很簡單,弱引用不會延長目标變量的生命周期。目标變量生命周期内(有強引用指着),多一個弱引用無所謂;反過來隻要變量超出生命周期,不再有強引用,那麼GC在每回合執行時,面對隻有弱引用的對象,執行一次回收一次,不會造成

Memory Leak

那麼,ThreadLocal的使用過程中就真的沒有記憶體洩露的風險了嗎?

回顧一下強軟弱虛4種引用關系:

強引用(Strong Reference)對象有強引用,對象不會被回收。

軟引用(Soft Reference)記憶體不夠時,會對僅有軟引用的對象進行回收,通常用作緩存伺服器。

弱引用(Weak Reference)記憶體回收線程一旦周期性的開始工作,每次執行都會将隻有軟引用的對象進行回收。也就是說,僅被弱引用關聯的對象隻能存活到下一次垃圾回收發生之前。

虛引用(Phantom Reference)完全不影響對象的回收。為一個對象設定虛引用關聯的唯一目的就是能在這個對象被回收時收到一個系統通知。

ThreadLocal記憶體洩露分析

通過前面的分析,ThreadLocalMap中存放的每一條Entry,都是以弱引用的方式對ThreadLocal<?>類型的key進行了包裝,看似不會造成記憶體洩露。但是别忘了,Entry中除了key,還存在對value的強引用。隻要線程還在,線程中的ThreadLocalMap對象,以及其中加入的Entry就會一直存在,不管Entry中引用的key是否已經被回收,value會一直存在。value這個強引用,如果不置為null,其引用的對象就不會被回收,造成記憶體洩露。

總之,需要記住一點:ThreadLocal.set(x)包裝一個對象x,會将這個對象x的存活期延長到一個線程的整個生命周期(該對象被丢進了線程特有的ThreadLocalMap的,一條Entry的Value中,而Entry對Value是強引用。是以必然會有這個結論)。

ThreadLocal有兩種使用方式:

1、跟随線程生命周期的ThreadLocal,被包裝的對象在整個線程的生命周期内都是被需要的,這種情況不會造成記憶體洩露。

2、如果隻是臨時使用ThreadLocal,即ThreadLocal變量的生命周期不需要伴随整個線程的話,應該回收自定義的 ThreadLocal 變量,否則可能會影響後續業務邏輯和造成記憶體洩露。

// shareObj隻需要在特定一段時間内被線程内共享,varThreadLocal是臨時變量,不跟随整個線程的生命周期
// 這種情況需要在使用完畢之後remove掉,否則,當varThreadLocal作為臨時變量被銷毀後,盡管Entry中的key所引用的對象變成了null,
// 但是Value依然是強引用指向shareObj,即shareObj未被正确回收。造成記憶體洩露。
varThreadLocal.set(shareObj);
try {
	// ...
} finally {
    // remove讓相應Entry中的value指向null,解除對shareObj的強引用
	varThreadLocal.remove();
}
           

總結:

1、每個線程内部調用

ThreadLocal.set()

方法,可以将變量存放到目前Thread對象内一個私有的容器(

ThreadLocalMap

)中。

get()

也是從目前線程私有的容器中拿取。

2、一個

ThreadLocal

變量雖然看起來是全局變量,但它内部包裝的值(Object)卻做到了每個線程私藏一份。每個線程都隻能讀寫自己線程的獨立副本,互不幹擾。

3、

ThreadLocal

一個常用的舉例是,可以用來存放目前線程持有的跟資料庫的連接配接(

Connection

)對象。這樣能保證:**一個事務的開始、送出、復原必須在同一個連接配接中完成。**一個線程一開始先從連接配接池拿到一個

Connection

對象,放進自己的

ThreadLocal<Connection>

中;在執行事務過程中,任何時刻從

ThreadLocal.get()

拿到的都是同一個

Connection

對象,這樣才能保證這個事務的開始、送出都是經由同一個

Connection

完成。

最後想說一點是,了解了

ThreadLocal

的原理之後,要實作線程私有的變量完全可以通過一個

HashMap

來實作(需要加鎖)。Map的key就是線程的Id,value就是要線上程内部獨占一份的變量。