Java 多线程 —— ThreadLocal
一、引言
ThreadLocal是Java幫助實(shí)現(xiàn)線程封閉性的典型手段。
作用:提供線程內(nèi)的局部變量,這種變量在線程的生命周期內(nèi)起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或組件之間一些公共變量的傳遞復(fù)雜度。同時(shí)也用來(lái)維護(hù)線程中的變量不被其他線程干擾。
這個(gè)類(lèi)能使線程中的某個(gè)值與保存值的對(duì)象關(guān)聯(lián)起來(lái)。ThreadLocal提供了get 與set方法,這些方法為每個(gè)使用該變量的線程都存有一份獨(dú)立的副本,因此get總是返回由當(dāng)前執(zhí)行線程在調(diào)用set時(shí)設(shè)置的最新值。
二、ThreadLocal的簡(jiǎn)單應(yīng)用
?ThreadLocal是使用空間換時(shí)間,synchronized是使用時(shí)間換空間,比如在hibernate中session就存在于ThreadLocal中,避免synchronized的使用。
下面程序的輸出結(jié)果為null,因?yàn)閺腡hreadLocal中取出的對(duì)象一定是本線程中set的對(duì)象,別的線程無(wú)法取出,因?yàn)榫€程自己放入的對(duì)象只能自己取得,因此無(wú)需進(jìn)行加鎖處理,執(zhí)行效率上ThreadLocal比synchronized要高。
public class ThreadLocal_02 {static ThreadLocal<Person> tl = new ThreadLocal<>();public static void main(String[] args) {new Thread(() -> {try {TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();}System.out.println(tl.get()); // output : null}).start();new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}tl.set(new Person("張三"));}).start();}static class Person {String name;public Person(String name) {this.name = name;}} }三、對(duì)ThreadLocal的理解
ThreadLocal對(duì)象通常用于防止對(duì)可變的單例變量或全局變量進(jìn)行共享。
例如,在單線程應(yīng)用程序中可能會(huì)維持一個(gè)全局的數(shù)據(jù)庫(kù)連接,并在程序啟動(dòng)時(shí)初始化這個(gè)連接對(duì)象,從而避免在調(diào)用每個(gè)方法時(shí)都要傳遞一個(gè)Connection對(duì)象。由于JDBC的連接對(duì)象不一定是線程安全的,因此,當(dāng)多線程應(yīng)用程序在沒(méi)有協(xié)同的情況下使用全局變量時(shí),就不是線程安全的。通過(guò)將JDBC的連接保存到ThreadLocal對(duì)象中,每個(gè)線程都會(huì)擁有屬于自己的連接:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){public Connection initialValue() {return DriverManager.getConnection(DB_URL);}};public static Connection getConnection() {return connectionHolder.get();}在比如,當(dāng)某個(gè)頻繁執(zhí)行的操作需要一個(gè)臨時(shí)對(duì)象,例如一個(gè)緩沖區(qū),而同時(shí)又希望避免在每次執(zhí)行時(shí)都重新分配該臨時(shí)對(duì)象,就可以使用ThreadLocal。
四、ThreadLocal的實(shí)現(xiàn)原理
ThreadLocal內(nèi)部提供了四個(gè)對(duì)外開(kāi)放的接口方法,這也是用戶(hù)操作ThreadLocal對(duì)象的基本方法:
1、public T get() :取得線程局部變量
2、public void set(T value) :設(shè)置線程局部變量
3、public void remove() :刪除線程局部變量
4、protected T initialValue() :返回該線程局部變量初始值
思考:ThreadLocal的實(shí)例是如何為每一個(gè)線程維護(hù)變量副本的呢?
上圖來(lái)自http://www.importnew.com/22039.html
其實(shí),每一個(gè)線程Thread其內(nèi)部都維護(hù)一個(gè)ThreadLocal.ThreadLocalMap的實(shí)例對(duì)象(變量名為:threadLocals)。
你可以將這個(gè)ThreadLocalMap對(duì)象理解為一個(gè)Map,但實(shí)際上它是一個(gè)數(shù)組,一個(gè)以封裝了ThreadLocal為鍵,Object為值的元素的數(shù)組。也就是說(shuō)ThreadLocal本身不存儲(chǔ)值,它只是作為一個(gè)key來(lái)讓當(dāng)前線程從ThreadLocalMap中獲取value。值得注意的是,ThreadLocalMap是使用?ThreadLocal的弱引用作為?Key?的,弱引用的對(duì)象在GC時(shí)會(huì)被回收。
static class ThreadLocalMap {//map中的每個(gè)節(jié)點(diǎn)Entry,其鍵key是ThreadLocal并且還是弱引用static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 初始化容量為16,以為對(duì)其擴(kuò)充也必須是2的指數(shù)private static final int INITIAL_CAPACITY = 16;// 真正用于存儲(chǔ)線程的每個(gè)ThreadLocal的數(shù)組,將ThreadLocal和其對(duì)應(yīng)的值包裝為一個(gè)Entryprivate Entry[] table;///....其他方法和操作都和map類(lèi)似 }由此,我們可以大概了解到了其線程局部變量的維護(hù)機(jī)制:為不同的線程創(chuàng)建不同的ThreadLocalMap,以線程本身作為區(qū)分,每個(gè)線程之間沒(méi)有任何聯(lián)系。
下面感興趣可以看一下get()、set()的源碼:
public T get() {Thread t = Thread.currentThread();//當(dāng)前線程ThreadLocalMap map = getMap(t);//獲取當(dāng)前線程對(duì)應(yīng)的ThreadLocalMapif (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);//獲取對(duì)應(yīng)ThreadLocal的變量值if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//若當(dāng)前線程還未創(chuàng)建ThreadLocalMap,則返回調(diào)用此方法并在其中調(diào)用createMap方法進(jìn)行創(chuàng)建并返回初始值。return setInitialValue(); } public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value); }五、ThreadLocal內(nèi)存泄漏問(wèn)題
5.1 ThreadLocal為什么會(huì)內(nèi)存泄漏?
ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個(gè)ThreadLocal沒(méi)有外部強(qiáng)引用來(lái)引用它,那么系統(tǒng) GC 的時(shí)候,這個(gè)ThreadLocal勢(shì)必會(huì)被回收,這樣一來(lái),ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒(méi)有辦法訪問(wèn)這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束(如線程池的線程回收)的話,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value?永遠(yuǎn)無(wú)法回收,造成內(nèi)存泄漏。?
其實(shí),ThreadLocalMap的設(shè)計(jì)中已經(jīng)考慮到這種情況,也加上了一些防護(hù)措施:在ThreadLocal的get(),set(),remove()的時(shí)候都會(huì)清除線程ThreadLocalMap里所有key為null的value。
但這些被動(dòng)的預(yù)防措施并不能保證不會(huì)內(nèi)存泄漏。
5.2 為什么使用弱引用?
從表面上看內(nèi)存泄漏的根源在于使用了弱引用。網(wǎng)上的文章大多著重分析ThreadLocal使用了弱引用會(huì)導(dǎo)致內(nèi)存泄漏,但是另一個(gè)問(wèn)題也同樣值得思考:為什么使用弱引用而不是強(qiáng)引用?
我們先來(lái)看看官方文檔的說(shuō)法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
為了應(yīng)對(duì)非常大和長(zhǎng)時(shí)間的用途,哈希表使用弱引用的 key。
下面我們分兩種情況討論:
- key 使用強(qiáng)引用:引用的ThreadLocal的對(duì)象被回收了,但是ThreadLocalMap還持有ThreadLocal的強(qiáng)引用,如果沒(méi)有手動(dòng)刪除,ThreadLocal不會(huì)被回收,導(dǎo)致Entry內(nèi)存泄漏。
- key 使用弱引用:引用的ThreadLocal的對(duì)象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒(méi)有手動(dòng)刪除,ThreadLocal也會(huì)被回收。value在下一次ThreadLocalMap調(diào)用set,get,remove的時(shí)候會(huì)被清除。
比較兩種情況,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng),如果都沒(méi)有手動(dòng)刪除對(duì)應(yīng)key,都會(huì)導(dǎo)致內(nèi)存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會(huì)內(nèi)存泄漏,對(duì)應(yīng)的value在下一次ThreadLocalMap調(diào)用set,get,remove的時(shí)候會(huì)被清除。
因此,ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng),如果沒(méi)有手動(dòng)刪除對(duì)應(yīng)Key就會(huì)導(dǎo)致內(nèi)存泄漏,而不是因?yàn)槿跻谩?/p>
5.3 有效避免內(nèi)存泄漏的最佳實(shí)踐
每次使用完ThreadLocal,都調(diào)用它的remove()方法,清除數(shù)據(jù)。
六、鳴謝
《深入剖析ThreadLocal實(shí)現(xiàn)原理以及內(nèi)存泄漏問(wèn)題》
《深入分析 ThreadLocal 內(nèi)存泄漏問(wèn)題》
總結(jié)
以上是生活随笔為你收集整理的Java 多线程 —— ThreadLocal的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Java面试宝典————基础篇
- 下一篇: java美元兑换,(Java实现) 美元