日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

深入理解ThreadLocal

發(fā)布時(shí)間:2024/1/17 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入理解ThreadLocal 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

學(xué)習(xí)JDK中的類,首先看下JDK API對(duì)此類的描述,描述如下:
該類提供了線程局部 (thread-local) 變量。這些變量不同于它們的普通對(duì)應(yīng)物,因?yàn)樵L問某個(gè)變量(通過其 get 或 set 方法)的每個(gè)線程都有自己的局部變量,它獨(dú)立于變量的初始化副本。ThreadLocal其實(shí)就是一個(gè)工具類,用來操作線程局部變量,ThreadLocal 實(shí)例通常是類中的 private static 字段。它們希望將狀態(tài)與某一個(gè)線程(例如,用戶 ID 或事務(wù) ID)相關(guān)聯(lián)。 例如,以下類生成對(duì)每個(gè)線程唯一的局部標(biāo)識(shí)符。線程 ID 是在第一次調(diào)用UniqueThreadIdGenerator.getCurrentThreadId()時(shí)分配的,在后續(xù)調(diào)用中不會(huì)更改。

其實(shí)ThreadLocal并非是一個(gè)線程的本地實(shí)現(xiàn)版本,它并不是一個(gè)Thread,而是threadlocalvariable(線程局部變量)。也許把它命名為ThreadLocalVar更加合適。線程局部變量(ThreadLocal)其實(shí)的功用非常簡(jiǎn)單,就是為每一個(gè)使用該變量的線程都提供一個(gè)變量值的副本,是Java中一種較為特殊的線程綁定機(jī)制,是每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)和其它線程的副本沖突。

?
  • import java.util.concurrent.atomic.AtomicInteger;

  • ?
  • public class UniqueThreadIdGenerator {

  • ?
  • private static final AtomicInteger uniqueId = new AtomicInteger(0);

  • ?
  • private static final ThreadLocal < Integer > uniqueNum =

  • new ThreadLocal < Integer > () {

  • @Override protected Integer initialValue() {

  • return uniqueId.getAndIncrement();

  • }

  • };

  • ?
  • public static int getCurrentThreadId() {

  • return uniqueId.get();

  • }

  • }

  • 從線程的角度看,每個(gè)線程都保持對(duì)其線程局部變量副本的隱式引用,只要線程是活動(dòng)的并且 ThreadLocal 實(shí)例是可訪問的;在線程消失之后,其線程局部實(shí)例的所有副本都會(huì)被垃圾回收(除非存在對(duì)這些副本的其他引用)。

    API表達(dá)了下面幾種觀點(diǎn):

  • ThreadLocal不是線程,是線程的一個(gè)變量,你可以先簡(jiǎn)單理解為線程類的屬性變量。
  • ThreadLocal 在類中通常定義為靜態(tài)類變量。
  • 每個(gè)線程有自己的一個(gè)ThreadLocal,它是變量的一個(gè)‘拷貝’,修改它不影響其他線程。
  • 既然定義為類變量,為何為每個(gè)線程維護(hù)一個(gè)副本(姑且成為‘拷貝’容易理解),讓每個(gè)線程獨(dú)立訪問?多線程編程的經(jīng)驗(yàn)告訴我們,對(duì)于線程共享資源(你可以理解為屬性),資源是否被所有線程共享,也就是說這個(gè)資源被一個(gè)線程修改是否影響另一個(gè)線程的運(yùn)行,如果影響我們需要使用synchronized同步,讓線程順序訪問。

    ThreadLocal適用于資源共享但不需要維護(hù)狀態(tài)的情況,也就是一個(gè)線程對(duì)資源的修改,不影響另一個(gè)線程的運(yùn)行;這種設(shè)計(jì)是空間換時(shí)間,synchronized順序執(zhí)行是時(shí)間換取空間。

    ThreadLocal介紹

    從字面上來理解ThreadLocal,感覺就是相當(dāng)于線程本地的。我們都知道,每個(gè)線程在jvm的虛擬機(jī)里都分配有自己獨(dú)立的空間,線程之間對(duì)于本地的空間是相互隔離的。那么ThreadLocal就應(yīng)該是該線程空間里本地可以訪問的數(shù)據(jù)了。ThreadLocal變量高效地為每個(gè)使用它的線程提供單獨(dú)的線程局部變量值的副本。每個(gè)線程只能看到與自己相聯(lián)系的值,而不知道別的線程可能正在使用或修改它們自己的副本。

    很多人看到這里會(huì)容易產(chǎn)生一種錯(cuò)誤的印象,感覺是不是這個(gè)ThreadLocal對(duì)象建立了一個(gè)類似于全局的map,然后每個(gè)線程作為map的key來存取對(duì)應(yīng)線程本地的value。你看,每個(gè)線程不一樣,所以他們映射到map中的key應(yīng)該也不一樣。實(shí)際上,如果我們后面詳細(xì)分析ThreadLocal的代碼時(shí),會(huì)發(fā)現(xiàn)不是這樣的。它具體是怎么實(shí)現(xiàn)的呢?

    ThreadLocal源碼

    ThreadLocal類本身定義了有g(shù)et(), set()和initialValue()三個(gè)方法。前面兩個(gè)方法是public的,initialValue()是protected的,主要用于我們?cè)诙xThreadLocal對(duì)象的時(shí)候根據(jù)需要來重寫。這樣我們初始化這么一個(gè)對(duì)象在里面設(shè)置它的初始值時(shí)就用到這個(gè)方法。ThreadLocal變量因?yàn)楸旧矶ㄎ粸橐欢鄠€(gè)線程來訪問,它通常被定義為static變量。

    ThreadLocal有一個(gè)ThreadLocalMap靜態(tài)內(nèi)部類,你可以簡(jiǎn)單理解為一個(gè)MAP,這個(gè)Map為每個(gè)線程復(fù)制一個(gè)變量的‘拷貝’存儲(chǔ)其中。

    當(dāng)線程調(diào)用ThreadLocal.get()方法獲取變量時(shí),首先獲取當(dāng)前線程引用,以此為key去獲取響應(yīng)的ThreadLocalMap,如果此‘Map’不存在則初始化一個(gè),否則返回其中的變量,代碼如下:

    ?
  • public T get() {

  • Thread t = Thread.currentThread();

  • ThreadLocalMap map = getMap(t);

  • if (map != null) {

  • ThreadLocalMap.Entry e = map.getEntry(this);

  • if (e != null)

  • return (T)e.value;

  • }

  • return setInitialValue();

  • }

  • 調(diào)用get方法如果此Map不存在首先初始化,創(chuàng)建此map,將線程為key,初始化的vlaue存入其中,注意此處的initialValue,我們可以覆蓋此方法,在首次調(diào)用時(shí)初始化一個(gè)適當(dāng)?shù)闹?。setInitialValue代碼如下:

    ?
  • private T setInitialValue() {

  • T value = initialValue();

  • Thread t = Thread.currentThread();

  • ThreadLocalMap map = getMap(t);

  • if (map != null)

  • map.set(this, value);

  • else

  • createMap(t, value);

  • return value;

  • }

  • set方法相對(duì)比較簡(jiǎn)單如果理解以上倆個(gè)方法,獲取當(dāng)前線程的引用,從map中獲取該線程對(duì)應(yīng)的map,如果map存在更新緩存值,否則創(chuàng)建并存儲(chǔ),代碼如下:

    ?
  • public void set(T value) {

  • Thread t = Thread.currentThread();

  • ThreadLocalMap map = getMap(t);

  • if (map != null)

  • map.set(this, value);

  • else

  • createMap(t, value);

  • }

  • 對(duì)于ThreadLocal在何處存儲(chǔ)變量副本,我們看getMap方法:獲取的是當(dāng)前線程的ThreadLocal類型的threadLocals屬性。顯然變量副本存儲(chǔ)在每一個(gè)線程中。

    ?
  • /**

  • * 獲取線程的ThreadLocalMap 屬性實(shí)例

  • */

  • ThreadLocalMap getMap(Thread t) {

  • return t.threadLocals;

  • }

  • 上面我們知道變量副本存放于何處,這里我們簡(jiǎn)單說下如何被java的垃圾收集機(jī)制收集,當(dāng)我們不在使用時(shí)調(diào)用set(null),此時(shí)不在將引用指向該‘map’,而線程退出時(shí)會(huì)執(zhí)行資源回收操作,將申請(qǐng)的資源進(jìn)行回收,其實(shí)就是將屬性的引用設(shè)置為null。這時(shí)已經(jīng)不在有任何引用指向該map,故而會(huì)被垃圾收集。

    注意:如果ThreadLocal.set()進(jìn)去的東西本來就是多個(gè)線程共享的同一個(gè)對(duì)象,那么多個(gè)線程的ThreadLocal.get()取得的還是這個(gè)共享對(duì)象本身,還是有并發(fā)訪問問題。

    看到ThreadLocal類中的變量只有這3個(gè)int型:

    ?
  • /**

  • * ThreadLocals rely on per-thread linear-probe hash maps attached

  • * to each thread (Thread.threadLocals and

  • * inheritableThreadLocals). The ThreadLocal objects act as keys,

  • * searched via threadLocalHashCode. This is a custom hash code

  • * (useful only within ThreadLocalMaps) that eliminates collisions

  • * in the common case where consecutively constructed ThreadLocals

  • * are used by the same threads, while remaining well-behaved in

  • * less common cases.

  • */

  • private final int threadLocalHashCode = nextHashCode();

  • ?
  • /**

  • * The next hash code to be given out. Updated atomically. Starts at

  • * zero.

  • */

  • private static AtomicInteger nextHashCode = new AtomicInteger();

  • ?
  • /**

  • * The difference between successively generated hash codes - turns

  • * implicit sequential thread-local IDs into near-optimally spread

  • * multiplicative hash values for power-of-two-sized tables.

  • */

  • private static final int HASH_INCREMENT = 0x61c88647;

  • 而作為ThreadLocal實(shí)例的變量只有 threadLocalHashCode 這一個(gè),nextHashCode 和HASH_INCREMENT 是ThreadLocal類的靜態(tài)變量,實(shí)際上HASH_INCREMENT是一個(gè)常量,表示了連續(xù)分配的兩個(gè)ThreadLocal實(shí)例的threadLocalHashCode值的增量,而nextHashCode 的表示了即將分配的下一個(gè)ThreadLocal實(shí)例的threadLocalHashCode 的值。

    現(xiàn)在來看看它的哈希策略。所有ThreadLocal對(duì)象共享一個(gè)AtomicInteger對(duì)象nextHashCode用于計(jì)算hashcode,一個(gè)新對(duì)象產(chǎn)生時(shí)它的hashcode就確定了,算法是從0開始,以HASH_INCREMENT = 0x61c88647為間隔遞增,這是ThreadLocal唯一需要同步的地方。根據(jù)hashcode定位桶的算法是將其與數(shù)組長(zhǎng)度-1進(jìn)行與操作:key.threadLocalHashCode & (table.length - 1)。

    0x61c88647這個(gè)魔數(shù)是怎么確定的呢?

    ThreadLocalMap的初始長(zhǎng)度為16,每次擴(kuò)容都增長(zhǎng)為原來的2倍,即它的長(zhǎng)度始終是2的n次方,上述算法中使用0x61c88647可以讓hash的結(jié)果在2的n次方內(nèi)盡可能均勻分布,減少?zèng)_突的概率。

    可以來看一下創(chuàng)建一個(gè)ThreadLocal實(shí)例即new ThreadLocal()時(shí)做了哪些操作,從上面看到構(gòu)造函數(shù)ThreadLocal()里什么操作都沒有,唯一的操作是這句:

    private final int threadLocalHashCode = nextHashCode();

    那么nextHashCode()做了什么呢:

    ?
  • private static synchronized int nextHashCode() {

  • int h = nextHashCode;

  • nextHashCode = h + HASH_INCREMENT;

  • return h;

  • }

  • 就是將ThreadLocal類的下一個(gè)hashCode值即nextHashCode的值賦給實(shí)例的threadLocalHashCode,然后nextHashCode的值增加HASH_INCREMENT這個(gè)值。

    因此ThreadLocal實(shí)例的變量只有這個(gè)threadLocalHashCode,而且是final的,用來區(qū)分不同的ThreadLocal實(shí)例,ThreadLocal類主要是作為工具類來使用,那么ThreadLocal.set()進(jìn)去的對(duì)象是放在哪兒的呢?

    看一下上面的set()方法,兩句合并一下成為:

    ThreadLocalMap map = Thread.currentThread().threadLocals;

    這個(gè)ThreadLocalMap 類是ThreadLocal中定義的內(nèi)部類,但是它的實(shí)例卻用在Thread類中:

    ?
  • public class Thread implements Runnable {

  • ......

  • ?
  • /* ThreadLocal values pertaining to this thread. This map is maintained

  • * by the ThreadLocal class. */

  • ThreadLocal.ThreadLocalMap threadLocals = null;

  • ?
  • /*

  • * InheritableThreadLocal values pertaining to this thread. This map is

  • * maintained by the InheritableThreadLocal class.

  • */

  • ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

  • ......

  • }

  • 這就說明了其實(shí)每個(gè)Thread本身就包含了兩個(gè)ThreadLocalMap對(duì)象的引用。這一點(diǎn)非常重要。以后每個(gè)thread要訪問他們的local對(duì)象時(shí),就是訪問存在這個(gè)ThreadLocalMap里的value。

    ThreadLocalMap是定義在ThreadLocal類內(nèi)部的私有類,它是采用“開放定址法”解決沖突的hashmap。key是ThreadLocal對(duì)象。當(dāng)調(diào)用某個(gè)ThreadLocal對(duì)象的get或put方法時(shí),首先會(huì)從當(dāng)前線程中取出ThreadLocalMap,然后查找對(duì)應(yīng)的value:

    ?
  • public T get() {

  • Thread t = Thread.currentThread();

  • ThreadLocalMap map = getMap(t); //拿到當(dāng)前線程的ThreadLocalMap

  • if (map != null) {

  • ThreadLocalMap.Entry e = map.getEntry(this); // 以該ThreadLocal對(duì)象為key取value

  • if (e != null)

  • return (T)e.value;

  • }

  • return setInitialValue();

  • }

  • ThreadLocalMap getMap(Thread t) {

  • return t.threadLocals;

  • }

  • 再看這句:

    ?
  • if (map != null)

  • map.set(this, value);

  • 也就是將該ThreadLocal實(shí)例作為key,要保持的對(duì)象作為值,設(shè)置到當(dāng)前線程的ThreadLocalMap 中,get()方法同樣大家看了代碼也就明白了,ThreadLocalMap 類的代碼太多了,我就不帖了,自己去看源碼吧。

    自然想法實(shí)現(xiàn)

    一個(gè)非常自然想法是用一個(gè)線程安全的Map<Thread,Object>實(shí)現(xiàn):

    ?
  • class ThreadLocal {

  • private Map values = Collections.synchronizedMap(new HashMap());

  • ?
  • public Object get() {

  • Thread curThread = Thread.currentThread();

  • Object o = values.get(curThread);

  • if (o == null && !values.containsKey(curThread)) {

  • o = initialValue();

  • values.put(curThread, o);

  • }

  • return o;

  • }

  • ?
  • public void set(Object newValue) {

  • values.put(Thread.currentThread(), newValue);

  • }

  • }

  • 但這是非常naive的:

  • ThreadLocal本意是避免并發(fā),用一個(gè)全局Map顯然違背了這一初衷;
  • 用Thread當(dāng)key,除非手動(dòng)調(diào)用remove,否則即使線程退出了會(huì)導(dǎo)致:1)該Thread對(duì)象無法回收;2)該線程在所有ThreadLocal中對(duì)應(yīng)的value也無法回收。
    JDK 的實(shí)現(xiàn)剛好是反過來的:
  • 碰撞解決與神奇的0x61c88647

    既然ThreadLocal用map就避免不了沖突的產(chǎn)生。

    碰撞避免和解決

    這里碰撞其實(shí)有兩種類型:
    (1)只有一個(gè)ThreadLocal實(shí)例的時(shí)候(上面推薦的做法),當(dāng)向thread-local變量中設(shè)置多個(gè)值的時(shí)產(chǎn)生的碰撞,碰撞解決是通過開放定址法, 且是線性探測(cè)(linear-probe)。
    (2)多個(gè)ThreadLocal實(shí)例的時(shí)候,最極端的是每個(gè)線程都new一個(gè)ThreadLocal實(shí)例,此時(shí)利用特殊的哈希碼0x61c88647大大降低碰撞的幾率, 同時(shí)利用開放定址法處理碰撞。

    神奇的0x61c88647

    注意 0x61c88647 的利用主要是為了多個(gè)ThreadLocal實(shí)例的情況下用的。從ThreadLocal源碼中找出這個(gè)哈希碼所在的地方:

    ?
  • /**

  • * ThreadLocals rely on per-thread linear-probe hash maps attached

  • * to each thread (Thread.threadLocals and inheritableThreadLocals).

  • * The ThreadLocal objects act as keys, searched via threadLocalHashCode.

  • * This is a custom hash code (useful only within ThreadLocalMaps) that

  • * eliminates collisions in the common case where consecutively

  • * constructed ThreadLocals are used by the same threads,

  • * while remaining well-behaved in less common cases.

  • */

  • private final int threadLocalHashCode = nextHashCode();

  • ?
  • /**

  • * The next hash code to be given out. Updated atomically.

  • * Starts at zero.

  • */

  • private static AtomicInteger nextHashCode = new AtomicInteger();

  • ?
  • /**

  • * The difference between successively generated hash codes - turns

  • * implicit sequential thread-local IDs into near-optimally spread

  • * multiplicative hash values for power-of-two-sized tables.

  • */

  • private static final int HASH_INCREMENT = 0x61c88647;

  • ?
  • /**

  • * Returns the next hash code.

  • */

  • private static int nextHashCode() {

  • return nextHashCode.getAndAdd(HASH_INCREMENT);

  • }

  • 注意實(shí)例變量threadLocalHashCode, 每當(dāng)創(chuàng)建ThreadLocal實(shí)例時(shí)這個(gè)值都會(huì)累加 0x61c88647, 目的在上面的注釋中已經(jīng)寫的很清楚了:為了讓哈希碼能均勻的分布在2的N次方的數(shù)組里, 即Entry[] table。

    下面來看一下ThreadLocal怎么使用的這個(gè)threadLocalHashCode哈希碼的,下面是ThreadLocalMap靜態(tài)內(nèi)部類中的set方法的部分代碼:

    ?
  • // Set the value associated with key.

  • private void set(ThreadLocal key, Object value) {

  • ?
  • Entry[] tab = table;

  • int len = tab.length;

  • int i = key.threadLocalHashCode & (len-1);

  • ?
  • for (Entry e = tab[i]; e != null;

  • e = tab[i = nextIndex(i, len)]) {...}

  • ?
  • ...

  • key.threadLocalHashCode & (len-1)這么用是什么意思? 先看一下table數(shù)組的長(zhǎng)度吧:

    ?
  • /**

  • * The table, resized as necessary.

  • * table.length MUST always be a power of two.

  • */

  • private Entry[] table;

  • 哇,ThreadLocalMap 中 Entry[] table 的大小必須是2的N次方呀(len = 2^N),那 len-1 的二進(jìn)制表示就是低位連續(xù)的N個(gè)1, 那 key.threadLocalHashCode & (len-1) 的值就是 threadLocalHashCode 的低N位, 這樣就能均勻的產(chǎn)生均勻的分布? 我用python做個(gè)實(shí)驗(yàn)吧:

    ?
  • >>> HASH_INCREMENT = 0x61c88647

  • >>> def magic_hash(n):

  • ... for i in range(n):

  • ... nextHashCode = i * HASH_INCREMENT + HASH_INCREMENT

  • ... print nextHashCode & (n - 1),

  • ... print

  • ...

  • >>> magic_hash(16)

  • 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0

  • >>> magic_hash(32)

  • 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0

  • 產(chǎn)生的哈希碼分布真的是很均勻,而且沒有任何沖突啊, 太神奇了。

    ThreadLocal內(nèi)存泄漏

    很多人認(rèn)為:threadlocal里面使用了一個(gè)存在弱引用的map,當(dāng)釋放掉threadlocal的強(qiáng)引用以后,map里面的value卻沒有被回收.而這塊value永遠(yuǎn)不會(huì)被訪問到了. 所以存在著內(nèi)存泄露. 最好的做法是將調(diào)用threadlocal的remove方法。

    說的也比較正確,當(dāng)value不再使用的時(shí)候,調(diào)用remove的確是很好的做法.但內(nèi)存泄露一說卻不正確. 這是threadlocal的設(shè)計(jì)的不得已而為之的問題. 首先,讓我們看看在threadlocal的生命周期中,都存在哪些引用吧. 看下圖: 實(shí)線代表強(qiáng)引用,虛線代表弱引用。

    每個(gè)thread中都存在一個(gè)map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key為一個(gè)threadlocal實(shí)例. 這個(gè)Map的確使用了弱引用,不過弱引用只是針對(duì)key. 每個(gè)key都弱引用指向threadlocal. 當(dāng)把threadlocal實(shí)例tl置為null以后,沒有任何強(qiáng)引用指向threadlocal實(shí)例,所以threadlocal將會(huì)被gc回收. 但是,我們的value卻不能回收,因?yàn)榇嬖谝粭l從current thread連接過來的強(qiáng)引用. 只有當(dāng)前thread結(jié)束以后, current thread就不會(huì)存在棧中,強(qiáng)引用斷開, Current Thread, Map, value將全部被GC回收。通過源碼看下此處的實(shí)現(xiàn),如下:

    ?
  • public void set(T value) {

  • Thread t = Thread.currentThread();

  • ThreadLocalMap map = getMap(t);

  • if (map != null)

  • map.set(this, value); // 將當(dāng)前threadLocal實(shí)例作為key

  • else

  • createMap(t, value);

  • }

  • ?
  • private void set(ThreadLocal key, Object value) {

  • ?
  • // We don't use a fast path as with get() because it is at

  • // least as common to use set() to create new entries as

  • // it is to replace existing ones, in which case, a fast

  • // path would fail more often than not.

  • ?
  • Entry[] tab = table;

  • int len = tab.length;

  • int i = key.threadLocalHashCode & (len-1);

  • ?
  • for (Entry e = tab[i];

  • e != null;

  • e = tab[i = nextIndex(i, len)]) {

  • ThreadLocal k = e.get();

  • ?
  • if (k == key) {

  • e.value = value;

  • return;

  • }

  • ?
  • if (k == null) {

  • replaceStaleEntry(key, value, i);

  • return;

  • }

  • }

  • ?
  • tab[i] = new Entry(key, value); // 構(gòu)造key-value實(shí)例

  • int sz = ++size;

  • if (!cleanSomeSlots(i, sz) && sz >= threshold)

  • rehash();

  • }

  • ?
  • static class Entry extends WeakReference<ThreadLocal> {

  • /** The value associated with this ThreadLocal. */

  • Object value;

  • ?
  • Entry(ThreadLocal k, Object v) {

  • super(k); // 構(gòu)造key弱引用

  • value = v;

  • }

  • }

  • ?
  • public T get() {

  • Thread t = Thread.currentThread();

  • ThreadLocalMap map = getMap(t);

  • if (map != null) {

  • ThreadLocalMap.Entry e = map.getEntry(this);

  • if (e != null)

  • return (T)e.value;

  • }

  • return setInitialValue();

  • }

  • ?
  • ThreadLocalMap getMap(Thread t) {

  • return t.threadLocals;

  • }

  • 從中可以看出,弱引用只存在于key上,所以key會(huì)被回收. 而value還存在著強(qiáng)引用.只有thead退出以后,value的強(qiáng)引用鏈條才會(huì)斷掉。一旦某個(gè)ThreadLocal對(duì)象沒有強(qiáng)引用了,它在所有線程內(nèi)部的ThreadLocalMap中的key都將被GC掉(此時(shí)value還未回收),在map后續(xù)的get/set中會(huì)探測(cè)到key被回收的entry,將其 value 設(shè)置為 null 以幫助GC,因此 value 在 key 被 GC 后可能還會(huì)存活一段時(shí)間,但最終也會(huì)被回收。這個(gè)過程和java.util.WeakHashMap的實(shí)現(xiàn)幾乎是一樣的。

    因此ThreadLocal本身是沒有內(nèi)存泄露問題的,通常由它引發(fā)的內(nèi)存泄露問題都是線程只 put 而忘了 remove 導(dǎo)致的,從上面分析可知,即使線程退出了,只要 ThreadLocal 還有強(qiáng)引用,該線程曾經(jīng) put 過的東西是不會(huì)被回收掉的。

    ThreadLocal有何用

    很多時(shí)候我們會(huì)創(chuàng)建一些靜態(tài)域來保存全局對(duì)象,那么這個(gè)對(duì)象就可能被任意線程訪問到,如果它是線程安全的,這當(dāng)然沒什么說的。然而大部分情況下它不是線程安全的(或者無法保證它是線程安全的),尤其是當(dāng)這個(gè)對(duì)象的類是由我們自己(或身邊的同事)創(chuàng)建的(很多開發(fā)人員對(duì)線程的知識(shí)都是一知半解,更何況線程安全)。

    這時(shí)候我們就需要為每個(gè)線程都創(chuàng)建一個(gè)對(duì)象的副本。我們當(dāng)然可以用ConcurrentMap 來保存這些對(duì)象,但問題是當(dāng)一個(gè)線程結(jié)束的時(shí)候我們?nèi)绾蝿h除這個(gè)線程的對(duì)象副本呢?

    ThreadLocal為我們做了一切。首先我們聲明一個(gè)全局的ThreadLocal對(duì)象(final static,沒錯(cuò),我很喜歡final),當(dāng)我們創(chuàng)建一個(gè)新線程并調(diào)用threadLocal.get時(shí),threadLocal會(huì)調(diào)用initialValue方法初始化一個(gè)對(duì)象并返回,以后無論何時(shí)我們?cè)谶@個(gè)線程中調(diào)用get方法,都將得到同一個(gè)對(duì)象(除非期間set過)。而如果我們?cè)诹硪粋€(gè)線程中調(diào)用get,將的到另一個(gè)對(duì)象,而且始終會(huì)得到這個(gè)對(duì)象。

    當(dāng)一個(gè)線程結(jié)束了,ThreadLocal就會(huì)釋放跟這個(gè)線程關(guān)聯(lián)的對(duì)象,這不需要我們關(guān)心,反正一切都悄悄地發(fā)生了。

    (以上敘述只關(guān)乎線程,而不關(guān)乎get和set是在哪個(gè)方法中調(diào)用的。以前有很多不理解線程的同學(xué)總是問我這個(gè)方法是哪個(gè)線程,那個(gè)方法是哪個(gè)線程,我不知如何回答。)

    所以,保存”線程局部變量”的map并非是ThreadLocal的成員變量, 而是java.lang.Thread的成員變量。也就是說,線程結(jié)束的時(shí)候,該map的資源也同時(shí)被回收。通過如下代碼:

    ThreadLocal的set,get方法中均通過如下方式獲取Map:

    ThreadLocalMap map = getMap(t);

    而getMap方法的代碼如下:

    ?
  • ThreadLocalMap getMap(Thread t) {

  • return t.threadLocals;

  • }

  • 可見:ThreadLocalMap實(shí)例是作為java.lang.Thread的成員變量存儲(chǔ)的,每個(gè)線程有唯一的一個(gè)threadLocalMap。這個(gè)map以ThreadLocal對(duì)象為key,”線程局部變量”為值,所以一個(gè)線程下可以保存多個(gè)”線程局部變量”。對(duì)ThreadLocal的操作,實(shí)際委托給當(dāng)前Thread,每個(gè)Thread都會(huì)有自己獨(dú)立的ThreadLocalMap實(shí)例,存儲(chǔ)的倉庫是Entry[] table;Entry的key為ThreadLocal,value為存儲(chǔ)內(nèi)容;因此在并發(fā)環(huán)境下,對(duì)ThreadLocal的set或get,不會(huì)有任何問題。以下為”線程局部變量”的存儲(chǔ)圖:

    由于treadLocalMap是java.util.Thread的成員變量,threadLocal作為threadLocalMap中的key值,在一個(gè)線程中只能保存一個(gè)”線程局部變量”。將ThreadLocalMap作為Thread類的成員變量的好處是:

    • 當(dāng)線程死亡時(shí),threadLocalMap被回收的同時(shí),保存的”線程局部變量”如果不存在其它引用也可以同時(shí)被回收。
    • 同一個(gè)線程下,可以有多個(gè)treadLocal實(shí)例,保存多個(gè)”線程局部變量”。
    • 同一個(gè)threadLocal實(shí)例,可以有多個(gè)線程使用,保存多個(gè)線程的“線程局部變量”。

    ThreadLocal的應(yīng)用

    我們?cè)诙嗑€程的開發(fā)中,經(jīng)常會(huì)考慮到的策略是對(duì)一些需要公開訪問的屬性通過設(shè)置同步的方式來訪問。這樣每次能保證只有一個(gè)線程訪問它,不會(huì)有沖突。但是這樣做的結(jié)果會(huì)使得性能和對(duì)高并發(fā)的支持不夠。在某些情況下,如果我們不一定非要對(duì)一個(gè)變量共享不可,而是給每個(gè)線程一個(gè)這樣的資源副本,讓他們可以獨(dú)立都各自跑各自的,這樣不是可以大幅度的提高并行度和性能了嗎?

    還有的情況是有的數(shù)據(jù)本身不是線程安全的,或者說它只能被一個(gè)線程使用,不能被其他線程同時(shí)使用。如果等一個(gè)線程使用完了再給另外一個(gè)線程使用就根本不現(xiàn)實(shí)。這樣的情況下,我們也可以考慮用ThreadLocal。一個(gè)典型的情況就是我們連接數(shù)據(jù)庫的時(shí)候通常會(huì)用到連接池。而對(duì)數(shù)據(jù)庫的連接不能有多個(gè)線程共享訪問。這個(gè)時(shí)候就需要使用ThreadLocal了。在比較熟悉的兩個(gè)框架中,Struts2和Hibernate均有采用ThreadLocal變量,而且對(duì)整個(gè)框架來說是非常核心的一部分。

    Struts2和Struts1的一個(gè)重要升級(jí)就是對(duì)request,response兩個(gè)對(duì)象的解耦,Struts2的Action方法中不再需要傳遞request,response參數(shù)。但是Struts2不通過方法直接傳入request,response對(duì)象,那么這兩個(gè)值是如何傳遞的呢?

    Struts2采用的正是ThreadLocal變量。在每次接收到請(qǐng)求時(shí),Struts2在調(diào)用攔截器和action前,通過將request,response對(duì)象放入ActionContext實(shí)例中,而ActionContext實(shí)例是作為”線程局部變量”存入ThreadLocal actionContext中。

    ?
  • public class ActionContext implements Serializable {

  • static ThreadLocal actionContext = new ThreadLocal();

  • . . .

  • 由于actionContext是”線程局部變量”,這樣我們通過ServletActionContext.getRequest()即可獲得本線程的request對(duì)象,而且在本地線程的任意類中,均可通過該方法獲取”線程局部變量”,而無需值傳遞,這樣Action類既可以成為一個(gè)simple類,無需繼承struts2的任意父類。

    在利用Hibernate開發(fā)DAO模塊時(shí),我們和Session打的交道最多,所以如何合理的管理Session,避免Session的頻繁創(chuàng)建和銷毀,對(duì)于提高系統(tǒng)的性能來說是非常重要的。一般常用的Hibernate工廠類,都會(huì)通過ThreadLocal來保存線程的session,這樣我們?cè)谕粋€(gè)線程中的處理,工廠類的getSession()方法,即可以多次獲取同一個(gè)Session進(jìn)行操作,closeSession方法可在不傳入?yún)?shù)的情況下,正確關(guān)閉session。

    Hiberante的Session 工具類HibernateUtil,這個(gè)類是Hibernate官方文檔中HibernateUtil類,用于session管理。如下:

    ?
  • public class HibernateUtil {

  • private static Log log = LogFactory.getLog(HibernateUtil.class);

  • private static final SessionFactory sessionFactory; //定義SessionFactory

  • ?
  • static {

  • try {

  • // 通過默認(rèn)配置文件hibernate.cfg.xml創(chuàng)建SessionFactory

  • sessionFactory = new Configuration().configure().buildSessionFactory();

  • } catch (Throwable ex) {

  • log.error("初始化SessionFactory失敗!", ex);

  • throw new ExceptionInInitializerError(ex);

  • }

  • }

  • ?
  • //創(chuàng)建線程局部變量session,用來保存Hibernate的Session

  • public static final ThreadLocal session = new ThreadLocal();

  • ?
  • /**

  • * 獲取當(dāng)前線程中的Session

  • * @return Session

  • * @throws HibernateException

  • */

  • public static Session currentSession() throws HibernateException {

  • Session s = (Session) session.get();

  • // 如果Session還沒有打開,則新開一個(gè)Session

  • if (s == null) {

  • s = sessionFactory.openSession();

  • session.set(s); //將新開的Session保存到線程局部變量中

  • }

  • return s;

  • }

  • ?
  • public static void closeSession() throws HibernateException {

  • //獲取線程局部變量,并強(qiáng)制轉(zhuǎn)換為Session類型

  • Session s = (Session) session.get();

  • session.set(null);

  • if (s != null)

  • s.close();

  • }

  • }

  • 在這個(gè)類中,由于沒有重寫ThreadLocal的initialValue()方法,則首次創(chuàng)建線程局部變量session其初始值為null,第一次調(diào)用currentSession()的時(shí)候,線程局部變量的get()方法也為null。因此,對(duì)session做了判斷,如果為null,則新開一個(gè)Session,并保存到線程局部變量session中,這一步非常的關(guān)鍵,這也是“public static final ThreadLocal session = new ThreadLocal()”所創(chuàng)建對(duì)象session能強(qiáng)制轉(zhuǎn)換為Hibernate Session對(duì)象的原因。

    可以看到在getSession()方法中,首先判斷當(dāng)前線程中有沒有放進(jìn)去session,如果還沒有,那么通過sessionFactory().openSession()來創(chuàng)建一個(gè)session,再將session set到線程中,實(shí)際是放到當(dāng)前線程的ThreadLocalMap這個(gè)map中,這時(shí),對(duì)于這個(gè)session的唯一引用就是當(dāng)前線程中的那個(gè)ThreadLocalMap,而threadSession作為這個(gè)值的key,要取得這個(gè)session可以通過threadSession.get()來得到,里面執(zhí)行的操作實(shí)際是先取得當(dāng)前線程中的ThreadLocalMap,然后將threadSession作為key將對(duì)應(yīng)的值取出。這個(gè)session相當(dāng)于線程的私有變量,而不是public的。

    顯然,其他線程中是取不到這個(gè)session的,他們也只能取到自己的ThreadLocalMap中的東西。要是session是多個(gè)線程共享使用的,那還不亂套了。

    試想如果不用ThreadLocal怎么來實(shí)現(xiàn)呢?可能就要在action中創(chuàng)建session,然后把session一個(gè)個(gè)傳到service和dao中,這可夠麻煩的?;蛘呖梢宰约憾x一個(gè)靜態(tài)的map,將當(dāng)前thread作為key,創(chuàng)建的session作為值,put到map中,應(yīng)該也行,這也是一般人的想法,但事實(shí)上,ThreadLocal的實(shí)現(xiàn)剛好相反,它是在每個(gè)線程中有一個(gè)map,而將ThreadLocal實(shí)例作為key,這樣每個(gè)map中的項(xiàng)數(shù)很少,而且當(dāng)線程銷毀時(shí)相應(yīng)的東西也一起銷毀了,不知道除了這些還有什么其他的好處。

    典型使用方式

    ?
  • // 摘自 j.u.c.ThreadLocalRandom

  • private static final ThreadLocal<ThreadLocalRandom> localRandom = // ThreadLocal對(duì)象都是static的,全局共享

  • new ThreadLocal<ThreadLocalRandom>() { // 初始值

  • protected ThreadLocalRandom initialValue() {

  • return new ThreadLocalRandom();

  • }

  • };

  • ?
  • localRandom.get(); // 拿當(dāng)前線程對(duì)應(yīng)的對(duì)象

  • localRandom.put(...); // put

  • ThreadLocal產(chǎn)生的問題

    在WEB服務(wù)器環(huán)境下,由于Tomcat,weblogic等服務(wù)器有一個(gè)線程池的概念,即接收到一個(gè)請(qǐng)求后,直接從線程池中取得線程處理請(qǐng)求;請(qǐng)求響應(yīng)完成后,這個(gè)線程本身是不會(huì)結(jié)束,而是進(jìn)入線程池,這樣可以減少創(chuàng)建線程、啟動(dòng)線程的系統(tǒng)開銷。

    由于Tomcat線程池的原因,我最初使用的”線程局部變量”保存的值,在下一次請(qǐng)求依然存在(同一個(gè)線程處理),這樣每次請(qǐng)求都是在本線程中取值而不是去memCache中取值,如果memCache中的數(shù)據(jù)發(fā)生變化,也無法及時(shí)更新。

    解決方案:處理完成后主動(dòng)調(diào)用該業(yè)務(wù)treadLocal的remove()方法,將”線程局部變量”清空,避免本線程下次處理的時(shí)候依然存在舊數(shù)據(jù)。

    Sturts2是如何解決線程池的問題呢?由于web服務(wù)器的線程是多次使用的,很顯然Struts2在響應(yīng)完成后,會(huì)主動(dòng)的清除“線程局部變量”中的ActionContext值,在struts2的org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter類中,有這樣的代碼片段:

    ?
  • finally {

  • prepare.cleanupRequest(request);

  • }

  • 而cleanupRequest方法中有如下代碼:

    ?
  • public void cleanupRequest(HttpServletRequest request) {

  • ……//省略部分代碼

  • ActionContext.setContext(null);

  • Dispatcher.setInstance(null);

  • }

  • 由此可見,Sturts2在處理完成后,會(huì)主動(dòng)清空”線程局部變量”ActionContext,來達(dá)到釋放系統(tǒng)資源的目的。

    ThreadLocal總結(jié)

    ThreadLocal使用場(chǎng)合主要解決多線程中數(shù)據(jù)數(shù)據(jù)因并發(fā)產(chǎn)生不一致問題。ThreadLocal為每個(gè)線程的中并發(fā)訪問的數(shù)據(jù)提供一個(gè)副本,通過訪問副本來運(yùn)行業(yè)務(wù),這樣的結(jié)果是耗費(fèi)了內(nèi)存,單大大減少了線程同步所帶來性能消耗,也減少了線程并發(fā)控制的復(fù)雜度。

    ThreadLocal不能使用原子類型,只能使用Object類型。ThreadLocal的使用比synchronized要簡(jiǎn)單得多。

    ThreadLocal和Synchonized都用于解決多線程并發(fā)訪問。但是ThreadLocal與synchronized有本質(zhì)的區(qū)別。synchronized是利用鎖的機(jī)制,使變量或代碼塊在某一時(shí)該只能被一個(gè)線程訪問。而ThreadLocal為每一個(gè)線程都提供了變量的副本,使得每個(gè)線程在某一時(shí)間訪問到的并不是同一個(gè)對(duì)象,這樣就隔離了多個(gè)線程對(duì)數(shù)據(jù)的數(shù)據(jù)共享。而Synchronized卻正好相反,它用于在多個(gè)線程間通信時(shí)能夠獲得數(shù)據(jù)共享。

    Synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。

    當(dāng)然ThreadLocal并不能替代synchronized,它們處理不同的問題域。Synchronized用于實(shí)現(xiàn)同步機(jī)制,比ThreadLocal更加復(fù)雜。

    總之,ThreadLocal不是用來解決對(duì)象共享訪問問題的,而主要是提供了保持對(duì)象的方法和避免參數(shù)傳遞的方便的對(duì)象訪問方式。歸納了兩點(diǎn):

  • 每個(gè)線程中都有一個(gè)自己的ThreadLocalMap類對(duì)象,可以將線程自己的對(duì)象保持到其中,各管各的,線程可以正確的訪問到自己的對(duì)象。
  • 將一個(gè)共用的ThreadLocal靜態(tài)實(shí)例作為key,將不同對(duì)象的引用保存到不同線程的ThreadLocalMap中,然后在線程執(zhí)行的各處通過這個(gè)靜態(tài)ThreadLocal實(shí)例的get()方法取得自己線程保存的那個(gè)對(duì)象,避免了將這個(gè)對(duì)象作為參數(shù)傳遞的麻煩。
  • ThreadLocal建議

  • ThreadLocal應(yīng)定義為靜態(tài)成員變量。
  • 能通過傳值傳遞的參數(shù),不要通過ThreadLocal存儲(chǔ),以免造成ThreadLocal的濫用。
  • 在線程池的情況下,在ThreadLocal業(yè)務(wù)周期處理完成時(shí),最好顯式的調(diào)用remove()方法,清空”線程局部變量”中的值。
  • 正常情況下使用ThreadLocal不會(huì)造成內(nèi)存溢出,弱引用的只是threadLocal,保存的值依然是強(qiáng)引用的,如果threadLocal依然被其他對(duì)象強(qiáng)引用,”線程局部變量”是無法回收的。
  • 本文引用自:http://www.kuqin.com/shuoit/20160421/351682.html

    總結(jié)

    以上是生活随笔為你收集整理的深入理解ThreadLocal的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。