理解Java中的ThreadLocal

前言

ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多??赡芎芏嗯笥讯贾繲hreadLocal為變量在每個線程中都創(chuàng)建了一個副本,那么每個線程可以訪問自己內(nèi)部的副本變量。

深入解析ThreadLocal類

在上面談到了對ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實現(xiàn)的。
先了解一下ThreadLocal類提供的幾個方法:

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

get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本,
set()用來設置當前線程中變量的副本,
remove()用來移除當前線程中變量的副本,
initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法,下面會詳細說明。

首先我們來看一下ThreadLocal類是如何為每個線程創(chuàng)建一個變量的副本的。
先看下get方法的實現(xiàn):


第一句是取得當前線程,然后通過getMap(t)方法獲取到一個map,map的類型為ThreadLocalMap。
然后接著下面獲取到<key,value>鍵值對,注意這里獲取鍵值對傳進去的是 this(即當前ThreadLocal對象),而不是當前線程t。
如果獲取成功,則返回value值。
如果map為空,則調(diào)用setInitialValue方法返回value。

我們上面的每一句來仔細分析:
首先看一下getMap方法中做了什么:



可能大家沒有想到的是,在getMap中,是調(diào)用當期線程t,返回當前線程t中的一個成員變量threadLocals。
那么我們繼續(xù)取Thread類中取看一下成員變量threadLocals是什么:



實際上就是一個ThreadLocalMap,這個類型是ThreadLocal類的一個內(nèi)部類,我們繼續(xù)取看ThreadLocalMap的實現(xiàn):

可以看到ThreadLocalMap的Entry繼承了WeakReference,并且使用ThreadLocal作為鍵值。

然后再繼續(xù)看setInitialValue方法的具體實現(xiàn):


很容易了解,就是如果map不為空,就設置鍵值對,為空,再創(chuàng)建Map,看一下createMap的實現(xiàn):


至此,可能大部分朋友已經(jīng)明白了ThreadLocal是如何為每個線程創(chuàng)建變量的副本的:

首先,在每個線程Thread內(nèi)部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當前ThreadLocal變量,value為變量副本(即T類型的變量)。

初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調(diào)用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。

然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。

下面通過一個例子來證明通過ThreadLocal能達到在每個線程中創(chuàng)建變量副本的效果:

package test;  
  
public class Test {  
      
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();  
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();  
  
    public void set() {  
        longLocal.set(Thread.currentThread().getId());  
        stringLocal.set(Thread.currentThread().getName());  
    }  
  
    public long getLong() {  
        return longLocal.get();  
    }  
  
    public String getString() {  
        return stringLocal.get();  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        final Test test = new Test();  
  
        test.set();  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
  
        Thread thread1 = new Thread() {  
            public void run() {  
                test.set();  
                System.out.println(test.getLong());  
                System.out.println(test.getString());  
            };  
        };  
        thread1.start();  
        thread1.join();  
  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
    }  
}

運行結(jié)果:

1  
main  
11  
Thread-0  
1  
main  

從這段代碼的輸出結(jié)果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣。最后一次在main線程再次打印副本值是為了證明在main線程中和thread1線程中的副本值確實是不同的。

總結(jié)一下

1)實際的通過ThreadLocal創(chuàng)建的副本是存儲在每個線程自己的threadLocals中的;
2)為何threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對象,因為每個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;
3)在進行g(shù)et之前,必須先set,否則會報空指針異常;
4)如果想在get之前不需要調(diào)用set就能正常訪問的話,必須重寫initialValue()方法。
因為在上面的代碼分析過程中,我們發(fā)現(xiàn)如果沒有先set的話,即在map中查找不到對應的存儲,則會通過調(diào)用setInitialValue方法返回i,而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認情況下,initialValue方法返回的是null。

ThreadLocal 存在的坑

1、沒有先set或者重寫initialValue方法時,直接get拿到的時null
如:

package test;  
  
public class Test02 {  
  
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();  
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();  
  
    public void set() {  
        longLocal.set(Thread.currentThread().getId());  
        stringLocal.set(Thread.currentThread().getName());  
    }  
  
    public long getLong() {  
        return longLocal.get();  
    }  
  
    public String getString() {  
        return stringLocal.get();  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        final Test02 test = new Test02();  
  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
  
        Thread thread1 = new Thread() {  
            public void run() {  
                test.set();  
                System.out.println(test.getLong());  
                System.out.println(test.getString());  
            };  
        };  
        thread1.start();  
        thread1.join();  
  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
    }  
}

運行結(jié)果:

Exception in thread "main" java.lang.NullPointerException  
    at test.Test02.getLong(Test02.java:14)  
    at test.Test02.main(Test02.java:24) 

在main線程中,沒有先set,直接get的話,拿到的是個null,然后getLong方法返回的值是基本類型long,這個時候會進行自動拆箱,直接報空指針異常。

但是如果改成下面這段代碼,即重寫了initialValue方法:

package test;  
  
public class Test03 {  
  
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>() {  
        protected Long initialValue() {  
            return Thread.currentThread().getId();  
        };  
    };  
      
    ThreadLocal<String> stringLocal = new ThreadLocal<String>() {  
        protected String initialValue() {  
            return Thread.currentThread().getName();  
        };  
    };  
  
    public void set() {  
        longLocal.set(Thread.currentThread().getId());  
        stringLocal.set(Thread.currentThread().getName());  
    }  
  
    public long getLong() {  
        return longLocal.get();  
    }  
  
    public String getString() {  
        return stringLocal.get();  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        final Test03 test = new Test03();  
  
        //test.set();  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
  
        Thread thread1 = new Thread() {  
            public void run() {  
                //test.set();  
                System.out.println(test.getLong());  
                System.out.println(test.getString());  
            };  
        };  
        thread1.start();  
        thread1.join();  
  
        System.out.println(test.getLong());  
        System.out.println(test.getString());  
    }  
}

運行結(jié)果:

1  
main  
8  
Thread-0  
1  
main 

2、在線程池環(huán)境下,由于線程是一直運行且復用的,使用ThreadLocal<T>時會出現(xiàn)這個任務看到上個任務ThreadLocal變量值以及內(nèi)存泄露等問題

ThreadLocal<T>變量一般要聲名成static類型,即當前線程中只有一個T類型變量的實例,線程內(nèi)可共享該實例數(shù)據(jù)且不會出問題,如將其聲名成非static,則一個線程內(nèi)就存儲多個T類型變量的實例,有點存儲空間的浪費,一般很少有這樣的應用場景。另外根據(jù)實際情況,ThreadLocal變量聲名時也多加上private final關(guān)鍵詞表明它時類內(nèi)私有、引用不可修改。

在線程池環(huán)境下,由于線程是一直運行且復用的,使用ThreadLocal<T>時會出現(xiàn)這個任務看到上個任務ThreadLocal變量值以及內(nèi)存泄露等問題,解決方法就是在當前任務執(zhí)行完后將ThreadLocal變量remove或設置為初始值。

通過上面的分析,我們能夠認識到ThreadLocal事實上是與線程綁定的一個變量,如此就會出現(xiàn)一個問題:假設沒有將ThreadLocal內(nèi)的變量刪除(remove)或替換,它的生命周期將會與線程共存,如果不remove掉,很可能會出現(xiàn)內(nèi)存泄漏的問題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 從這個類的名字就能大體了解到類的作用,ThreadLocal可以分解為Thread和Local,前者就不多說了,后...
    Acamy丶閱讀 2,439評論 0 5
  • 一、關(guān)于線程本地存儲 線程本地存儲是一種自動化機制,可以為使用相同變量的每個不同的線程都創(chuàng)建不同的存儲,通過根除對...
    阮小貳閱讀 4,027評論 0 2
  • 想必很多朋友對ThreadLocal并不陌生,今天我們就來一起探討下ThreadLocal的使用方法和實現(xiàn)原理。首...
    03ca2835cf70閱讀 220評論 0 0
  • ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲。ThreadLocal為變量在每個線...
    seuwt閱讀 743評論 0 50
  • 歲末年初,有心的人都在做總結(jié),同時制定來年的計劃。 最近比較浮躁,表面無比喧囂,內(nèi)里分外寂寥。一場持續(xù)好幾天的重感...
    暫且這樣吧閱讀 568評論 0 0

友情鏈接更多精彩內(nèi)容