<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
變數值的共用可以使用public static的形式,所有執行緒都使用同一個變數,如果想實現每一個執行緒都有自己的共用變數該如何實現呢?JDK中的ThreadLocal類正是為了解決這樣的問題。
ThreadLocal類並不是用來解決多執行緒環境下的共用變數問題,而是用來提供執行緒內部的共用變數,在多執行緒環境下,可以保證各個執行緒之間的變數互相隔離、相互獨立。線上程中,可以通過get()/set()方法來存取變數。ThreadLocal範例通常來說都是private static型別的,它們希望將狀態與執行緒進行關聯。這種變數線上程的生命週期內起作用,可以減少同一個執行緒內多個函數或者元件之間一些公共變數的傳遞的複雜度。
我們先通過一個例子來看一下ThreadLocal的基本用法:
public class ThreadLocalTest { static class MyThread extends Thread { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); @Override public void run() { super.run(); for (int i = 0; i < 3; i++) { threadLocal.set(i); System.out.println(getName() + " threadLocal.get() = " + threadLocal.get()); } } } public static void main(String[] args) { MyThread myThreadA = new MyThread(); myThreadA.setName("ThreadA"); MyThread myThreadB = new MyThread(); myThreadB.setName("ThreadB"); myThreadA.start(); myThreadB.start(); } }
執行結果(不唯一):
ThreadA threadLocal.get() = 0
ThreadB threadLocal.get() = 0
ThreadA threadLocal.get() = 1
ThreadA threadLocal.get() = 2
ThreadB threadLocal.get() = 1
ThreadB threadLocal.get() = 2
雖然兩個執行緒都在向threadLocal物件中set()資料值,但每個執行緒都還是能取出自己設定的資料,確實可以達到隔離執行緒變數的效果。
ThreadLocal常用方法介紹
當呼叫get()方法的時候,若是與當前執行緒關聯的ThreadLocal值已經被設定過,則不會呼叫initialValue()方法;否則,會呼叫initialValue()方法來進行初始值的設定。通常initialValue()方法只會被呼叫一次,除非呼叫了remove()方法之後又呼叫get()方法,此時,與當前執行緒關聯的ThreadLocal值處於沒有設定過的狀態(其狀態體現在原始碼中,就是執行緒的ThreadLocalMap物件是否為null),initialValue()方法仍會被呼叫。
initialValue()方法是protected型別的,很顯然是建議在子類過載該函數的,所以通常該方法都會以匿名內部類的形式被過載,以指定初始值,例如:
public class ThreadLocalTest { public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return Integer.valueOf(1); } }; }
remove()方法:將與當前執行緒關聯的ThreadLocal值刪除。
ThreadLocal最簡單的實現方式就是ThreadLocal類內部有一個執行緒安全的Map,然後用執行緒的ID作為Map的key,範例物件作為Map的value,這樣就能達到各個執行緒的值隔離的效果。
JDK最早期的ThreadLocal就是這樣設計的,但是,之後ThreadLocal的設計換了一種方式,我們先看get()方法的原始碼,然後進一步介紹ThreadLocal的實現方式:
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(); }
get()方法主要做了以下事情:
1、呼叫Thread.currentThread()獲取當前執行緒物件t;
2、根據當前執行緒物件,呼叫getMap(Thread)獲取執行緒對應的ThreadLocalMap物件:
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
threadLocals是Thread類的成員變數,初始化為null:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
3、如果獲取的map不為空,則在map中以ThreadLocal的參照作為key來在map中獲取對應的value e,否則轉到步驟5;
4、若e不為null,則返回e中儲存的value值,否則轉到步驟5;
5、呼叫setInitialValue()方法,對執行緒的ThreadLocalMap物件進行初始化操作,ThreadLocalMap物件的key為ThreadLocal物件,value為initialValue()方法的返回值。
從上面的分析中,可以看到,ThreadLocal的實現離不開ThreadLocalMap類,ThreadLocalMap類是ThreadLocal的靜態內部類。每個Thread維護一個ThreadLocalMap對映表,這個對映表的key是ThreadLocal範例本身,value是真正需要儲存的Object。這樣的設計主要有以下幾點優勢:
ThreadLocalMap是用來儲存與執行緒關聯的value的雜湊表,它具有HashMap的部分特性,比如容量、擴容閾值等,它內部通過Entry類來儲存key和value,Entry類的定義為:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry繼承自WeakReference,通過上述原始碼super(k);可以知道,ThreadLocalMap是使用ThreadLocal的弱參照作為Key的。
分析到這裡,我們可以得到下面這個物件之間的參照結構圖(其中,實線為強參照,虛線為弱參照):
我們知道,弱參照物件在Java虛擬機器器進行垃圾回收時,就會被釋放,那我們考慮這樣一個問題:
ThreadLocalMap使用ThreadLocal的弱參照作為key,如果一個ThreadLocal沒有外部關聯的強參照,那麼在虛擬機器器進行垃圾回收時,這個ThreadLocal會被回收,這樣,ThreadLocalMap中就會出現key為null的Entry,這些key對應的value也就再無妨存取,但是value卻存在一條從Current Thread過來的強參照鏈。因此只有當Current Thread銷燬時,value才能得到釋放。
該強參照鏈如下:
CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value
因此,只要這個執行緒物件被gc回收,那些key為null對應的value也會被回收,這樣也沒什麼問題,但線上程物件不被回收的情況下,比如使用執行緒池的時候,核心執行緒是一直在執行的,執行緒物件不會回收,若是在這樣的執行緒中存在上述現象,就可能出現記憶體洩露的問題。
那在ThreadLocalMap中是如何解決這個問題的呢?
在獲取key對應的value時,會呼叫ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,該方法原始碼如下:
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
通過key.threadLocalHashCode & (table.length - 1)來計算儲存key的Entry的索引位置,然後判斷對應的key是否存在,若存在,則返回其對應的value,否則,呼叫getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法,原始碼如下:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
ThreadLocalMap採用線性探查的方式來處理雜湊衝突,所以會有一個while迴圈去查詢對應的key,在查詢過程中,若發現key為null,即通過弱參照的key被回收了,會呼叫expungeStaleEntry(int)方法,其原始碼如下:
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
通過上述程式碼可以發現,若key為null,則該方法通過下述程式碼來清理與key對應的value以及Entry:
// expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null;
此時,CurrentThread Ref不存在一條到Entry物件的強參照鏈,Entry到value物件也不存在強參照,那在程式執行期間,它們自然也就會被回收。expungeStaleEntry(int)方法的後續程式碼就是以線性探查的方式,調整後續Entry的位置,同時檢查key的有效性。
在ThreadLocalMap中的set()/getEntry()方法中,都會呼叫expungeStaleEntry(int)方法,但是如果我們既不需要新增value,也不需要獲取value,那還是有可能產生記憶體漏失的。所以很多情況下需要使用者手動呼叫ThreadLocal的remove()函數,手動刪除不再需要的ThreadLocal,防止記憶體洩露。若對應的key存在,remove()方法也會呼叫expungeStaleEntry(int)方法,來刪除對應的Entry和value。
其實,最好的方式就是將ThreadLocal變數定義成private static的,這樣的話ThreadLocal的生命週期就更長,由於一直存在ThreadLocal的強參照,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱參照存取到Entry的value值,然後remove它,可以防止記憶體洩露。
InheritableThreadLocal繼承自ThreadLocal,使用InheritableThreadLocal類可以使子執行緒繼承父執行緒的值,來看一段範例程式碼:
public class ThreadLocalTest { private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() { @Override protected Integer initialValue() { return Integer.valueOf(10); } }; static class MyThread extends Thread { @Override public void run() { super.run(); System.out.println(getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get()); } } public static void main(String[] args) { System.out.println(Thread.currentThread().getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get()); MyThread myThread = new MyThread(); myThread.setName("執行緒A"); myThread.start(); } }
執行結果:
main inheritableThreadLocal.get() = 10
執行緒A inheritableThreadLocal.get() = 10
可以看到子執行緒成功繼承了父執行緒的值。
父執行緒還可以設定子執行緒的初始值,只需要重寫InheritableThreadLocal類的childValue(T)方法即可,將上述程式碼的inheritableThreadLocal 定義修改為如下方式:
private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() { @Override protected Integer initialValue() { return Integer.valueOf(10); } @Override protected Integer childValue(Integer parentValue) { return Integer.valueOf(5); } };
執行結果為:
main inheritableThreadLocal.get() = 10
執行緒A inheritableThreadLocal.get() = 5
可以看到,子程序成功獲取到了父程序設定的初始值。
使用InheritableThreadLocal類需要注意的一點是,如果子執行緒在取得值的同時,主執行緒將InheritableThreadLocal中的值進行更改,那子執行緒獲取的還是舊值。
執行緒中用來實現上述功能的ThreadLocalMap類變數為
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
InheritableThreadLocal類的實現很簡單,主要是重寫了ThreadLocal類的getMap(Thread)方法和createMap(Thread, T)方法,將其中操作的ThreadLocalMap變數修改為了inheritableThreadLocals,這裡不再進一步敘述。
高洪巖:《Java多執行緒程式設計核心技術》
到此這篇關於深入理解Java並行程式設計之ThreadLocal 的文章就介紹到這了,更多相關Java ThreadLocal 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45