<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
對於Android程式設計師來說,很多人都是在學習訊息機制時候瞭解到ThreadLocal這個東西的。那它有什麼作用呢?官方檔案大致是這麼描述的:
先來看一個簡單的使用例子吧:
public class ThreadId { private static final AtomicInteger nextId = new AtomicInteger(0); private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return nextId.get(); } }; public static int get() { return threadId.get(); } }
這也是官方檔案上的例子,非常簡單,就是通過在不同執行緒呼叫ThredId.get()可以獲取唯一的執行緒Id。如果在呼叫ThreadLocal的get方法之前沒有主動呼叫過set方法設定值的話,就會返回initialValue方法的返回值,並把這個值儲存為當前執行緒的變數。
ThreadLocal到底是用來解決什麼問題,適用什麼場景呢,例子是看懂了,但好像還是沒什麼體會?ThreadLocal既然是提供變數的,我們不妨把我們見過的變數型別拿出來,做個對比
變數型別 | 作用域 | 生命週期 | 執行緒共用性 | 作用 |
---|---|---|---|---|
區域性變數 | 方法(程式碼塊)內部,其他方法(程式碼塊)不能存取 | 方法(程式碼塊)開始到結束 | 只存在於每個執行緒的工作記憶體,不能線上程中共用 | 解決變數在方法(程式碼塊)內部的程式碼行之間的共用 |
成員變數 | 範例內 | 和範例相同 | 可線上程間共用 | 解決變數在實體方法之間的共用,否則方法之間只能靠引數傳遞變數 |
靜態變數 | 類內部 | 和類的生命週期相同 | 可在多個執行緒間共用 | 解決變數在多個範例之間的共用 |
ThreadLocal儲存的變數 | 整個執行緒 | 一般而言與執行緒的生命週期相同 | 不再多執行緒間共用 | 解決變數在單個執行緒中的共用問題,執行緒中處處可存取 |
ThreadLocal儲存的變數本質上間接算是Thread的成員變數,ThreadLocal只是提供了一種對開發者透明的可以為每個執行緒儲存同一維度成員變數的方式。
網上有很多人持有如下的看法: ThreadLocal為解決多執行緒程式的並行問題提供了一種新思路或者ThreadLocal是為了解決多執行緒存取資源時的共用問題。 個人認為這些都是錯誤的,ThreadLocal儲存的變數是執行緒隔離的,與資源共用沒有任何關係,也沒有解決什麼並行問題,這一點看了ThreadLocal的原理就會更加清楚。就好比上面的例子,每個執行緒應該有一個執行緒Id,這並不是什麼並行問題啊。
同時他們會拿ThreadLocal與sychronized做對比,我們要清楚它們根本不是為了解決同一類問題設計的。sychronized是在牽涉到共用變數時候,要做到執行緒間的同步,保證並行中的原子性與記憶體可見性,典型的特徵是多個執行緒會存取相同的變數。而ThreadLocal根本不是解決執行緒同步問題的,它的場景是A執行緒儲存的變數只有A執行緒需要存取,而其它的執行緒並不需要存取,其他執行緒也只存取自己儲存的變數。
我們來一個開放性的問題,假如現在要給每個執行緒增加一個執行緒Id,並且Java的Thread類你能隨便修改,你要怎麼操作?非常簡單吧,程式碼大概是這樣
public class Thread{ private int id; public void setId(int id){ this.id=id; } }
那好,現在題目變了,我們現在還得為每個執行緒儲存一個Looper物件,那怎麼辦呢?再加一個Looper的欄位不就好了,顯然這種做法肯定是不具有擴充套件性的。那我們用一個容器類不就好了,很自然地就會想到Map,像下面這樣
public class Thread{ private Map<String,Object> map; public Map<String,Object> getMap(){ if(map==null) map=new HashMap<>(); return map; } }
然後我們在程式碼裡就可以通過如下程式碼來給Thread設定“成員變數”了
Thread.currentThread().getMap().put("id",id); Thread.currentThread().getMap().put("looper",looper);
然後可以在該執行緒執行的任意地方,這樣存取:
Looper looper=(Looper) Thread.currentThread().getMap().get("looper");
看上去還不錯,但是還是有些問題:
為了不通過字串存取,同時省去強制轉換,我們封裝一個類,就叫ThreadLocal吧,虛擬碼如下:
public class ThreadLocal<T> { public void set(T value) { Thread t = Thread.currentThread(); Map map = t.getMap(); if (map != null) //以自己為鍵 map.put(this, value); else createMap(t, value); } public T get() { Thread t = Thread.currentThread(); Map<ThreadLocal<?>,T> map = t.getMap(); if (map != null) { T e = map.get(this); return e; } return setInitialValue(); } }
沒錯,以上基本上就是ThreadLocal的整體設計了,只是執行緒中儲存資料的Map是特意實現的ThreadLocal.ThreadLocalMap。
ThredLocal本身並不儲存變數,只是向每個執行緒的threadLocals中儲存鍵值對。ThreadLocal橫跨執行緒,提供一種類似切面的概念,這種切面是作用線上程上的。
我們對ThreadLocal已經有一個整體的認識了,接下來我們大致看一下原始碼
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
set方法通過Thread.currentThread方法獲取當前執行緒,然後呼叫getMap方法獲取執行緒的threadLocals欄位,並往ThreadLocalMap中放入鍵值對,其中鍵為ThreadLocal範例自己。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
接著看get方法:
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(); }
很清晰,其中值得注意的是最後一行的setInitialValue方法,這個方法在我們沒有呼叫過set方法時候呼叫。
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; }
setInitialValue方法會獲取initialValue的返回值並把它放進當前執行緒的threadLocals中。預設情況下initialValue返回null,我們可以實現這個方法來對變數進行初始化,就像上面TheadId的例子一樣。
remove方法,從當前執行緒的ThreadLocalMap中移除元素。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
看ThreadLocalMap的程式碼我們主要是關注以下兩個方面:
雜湊函數 先來理一下雜湊函數吧,我們在之後的程式碼中會看到ThreadLocalMap通過 int i = key.threadLocalHashCode & (len-1);
決定元素的位置,其中表大小len為2的冪,因此這裡的&操作相當於取模。另外我們關注的是threadLocalHashCode的取值。
private final int threadLocalHashCode = nextHashCode(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647;
這裡很有意思,每個ThreadLocal範例的threadLocalHashCode是在之前ThreadLocal範例的threadLocalHashCode上加 0x61c88647,為什麼偏偏要加這麼個數呢? 這個魔數的選取與斐波那契雜湊有關以及黃金分割法有關,具體不是很清楚。它的作用是這樣產生的值與2的冪取模後能在雜湊表中均勻分佈,即便擴容也是如此。看下面一段程式碼:
public class MagicHashCode { //ThreadLocal中定義的魔數 private static final int HASH_INCREMENT = 0x61c88647; public static void main(String[] args) { hashCode(16);//初始化16 hashCode(32);//2倍擴容 hashCode(64); } private static void hashCode(int length){ int hashCode = 0; for(int i=0;i<length;i++){ hashCode = i*HASH_INCREMENT+HASH_INCREMENT; System.out.print(hashCode & (length-1));//求取模後的下標 System.out.print(" "); } System.out.println(); } }
輸出結果為:
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 //容量為16時
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 //容量為32時
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 //容量為64時
因為ThreadLocalMap使用線性探測法解決衝突(下文會看到),均勻分佈的好處在於發生了衝突也能很快找到空的slot,提高效率。
瞄一眼成員變數:
/** * 初始容量,必須是2的冪。這樣的話,方便把取模運算轉化為與運算, * 效率高 */ private static final int INITIAL_CAPACITY = 16; /** * 容納Entry元素,長度必須是2的冪 */ private Entry[] table; /** * table中的元素個數. */ private int size = 0; /** * table裡的元素達到這個值就需要擴容了 * 其實是有個裝載因子的概念的 */ private int threshold; // Default to 0
建構函式:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
firstKey和firstValue就是Map存放的第一個鍵值對嘍。其中firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
很關鍵,就是當容量為2的冪時候,這相當於一個取模操作。然後把Entry儲存到陣列的第i個位置,設定擴容的閾值。
private void setThreshold(int len) { threshold = len * 2 / 3; }
這說明當陣列裡的元素容量達到2/3時候就要擴容,也就是裝載因子是2/3。 接下來我們來看下Entry
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
就這麼點東西,這個Entry只是與HashMap不同,只是個普通的鍵值對,沒有連結串列結構相關的東西。
另外Entry只持有對鍵,也就是ThreadLocal的弱參照,那麼我們上面的第二個問題算是有答案了。當沒有其他強參照指向ThreadLocal的時候,它其實是會被回收的。
但是這有引出了另外一個問題,那Entry呢?當鍵都為空的時候這個Entry也是沒有什麼作用啊,也應該被回收啊。不慌,我們接著往下看。
set方法:
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)]) { ThreadLocal<?> k = e.get(); //判斷鍵是否相等,相等的話只要更新值就好了 if (k == key) { e.value = value; return; } if (k == null) { //該Entry對應的ThreadLocal已經被回收,執行replaceStaleEntry並返回 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; //進行啟發式清理,如果沒有清理任何元素並且表的大小超過了閾值,需要擴容並重雜湊 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
我們發現如果發生衝突的話,整體邏輯會一直呼叫nextIndex方法去探測下一個位置,直到找到沒有元素的位置,邏輯上整個表是一個環形。下面是nextIndex的程式碼,就是加1而已。
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
線性探測的過程中,有一種情況是需要清理對應Entry的,也就是Entry的key為null,我們上面討論過這種情況下的Entry是無意義的。
因此呼叫 replaceStaleEntry(key, value, i);
在看replaceStaleEntry(key, value, i)我們先明確幾個問題。
採用線性探測發解決衝突,在插入過程中產生衝突的元素之前一定是沒有空的slot的。這樣在也確保在查詢過程,查詢到空的slot就可以停止啦。
但是假如我們刪除了一個元素,就會破壞這種情況,這時需要對錶中刪除的元素後面的元素進行再雜湊,以便填上空隙。
空slot:即該位置沒有元素
無效slot:該位置有元素,但key為null
replaceStaleEntry除了將value放入合適的位置之外,還會在前後連個空的slot之間做一次清理expungeStaleEntry,清理掉無效slot。
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // 向前掃描到一個空的slot為止,找到離這個空slot最近的無效slot,記錄為slotToExpunge int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) { if (e.get() == null) { slotToExpunge = i; } } // 向後遍歷table for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // 找到了key,將其與無效slot交換 if (k == key) { // 更新對應slot的value值 e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; //如果之前還沒有探測到過其他無效的slot if (slotToExpunge == staleSlot) { slotToExpunge = i; } // 從slotToExpunge開始做一次連續段的清理,再做一次啟發式清理 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // 如果當前的slot已經無效,並且向前掃描過程中沒有無效slot,則更新slotToExpunge為當前位置 if (k == null && slotToExpunge == staleSlot) { slotToExpunge = i; } } // 如果key之前在table中不存在,則放在staleSlot位置 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // 在探測過程中如果發現任何其他無效slot,連續段清理後做啟發式清理 if (slotToExpunge != staleSlot) { cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } }
expungeStaleEntry主要是清除連續段之前無效的slot,然後對元素進行再雜湊。返回下一個空的slot位置。
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 刪除 staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; 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; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
啟發式地清理: i對應是非無效slot(slot為空或者有效) n是用於控制控制掃描次數 正常情況下如果log n次掃描沒有發現無效slot,函數就結束了。 但是如果發現了無效的slot,將n置為table的長度len,做一次連續段的清理,再從下一個空的slot開始繼續掃描。
這個函數有兩處地方會被呼叫,一處是插入的時候可能會被呼叫,另外個是在替換無效slot的時候可能會被呼叫, 區別是前者傳入的n為實際元素個數,後者為table的總容量。
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { // i在任何情況下自己都不會是一個無效slot,所以從下一個開始判斷 i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { // 擴大掃描控制因子 n = len; removed = true; // 清理一個連續段 i = expungeStaleEntry(i); } } while ((n >>>= 1) != 0); return removed; }
接著看set函數,如果迴圈過程中沒有返回,找到合適的位置,插入元素,表的size增加1。這個時候會做一次啟發式清理,如果啟發式清理沒有清理掉任何無效元素,判斷清理前表的大小大於閾值threshold的話,正常就要進行擴容了,但是表中可能存在無效元素,先把它們清除掉,然後再判斷。
private void rehash() { // 全量清理 expungeStaleEntries(); //因為做了一次清理,所以size可能會變小,這裡的實現是調低閾值來判斷是否需要擴容。 threshold預設為len*2/3,所以這裡的threshold - threshold / 4相當於len/2。 if (size >= threshold - threshold / 4) { resize(); } }
作用即清除所有無效slot
private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) { expungeStaleEntry(j); } } }
保證table的容量len為2的冪,擴容時候要擴大2倍
private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; } else { // 擴容後要重新放置元素 int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) { h = nextIndex(h, newLen); } newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
get方法:
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 對應的entry存在且key未被回收 if (e != null && e.get() == key) { return e; } else { // 繼續往後查詢 return getEntryAfterMiss(key, i, e); } } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // 不斷向後探測直到遇到空entry while (e != null) { ThreadLocal<?> k = e.get(); // 找到 if (k == key) { return e; } if (k == null) { // 該entry對應的ThreadLocal範例已經被回收,呼叫expungeStaleEntry來清理無效的entry expungeStaleEntry(i); } else { // 下一個位置 i = nextIndex(i, len); } e = tab[i]; } return null; }
remove方法,比較簡單,在table中找key,如果找到了斷開弱參照,做一次連續段清理。
private void remove(ThreadLocal<?> key) { 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)]) { if (e.get() == key) { //斷開弱參照 e.clear(); // 連續段清理 expungeStaleEntry(i); return; } } }
從上文我們知道當呼叫ThreadLocalMap的set或者getEntry方法時候,有很大概率會去自動清除掉key為null的Entry,這樣就可以斷開value的強參照,使物件被回收。
但是如果如果我們之後再也沒有在該執行緒操作過任何ThreadLocal範例的set或者get方法,那麼就只能等執行緒死亡才能回收無效value。
因此當我們不需要用ThreadLocal的變數時候,顯示呼叫ThreadLocal的remove方法是一種好的習慣。
ThreadLocal.ThreadLocalMap threadLocals
中以上就是ThreadLocal作用原理與記憶體洩露範例解析的詳細內容,更多關於ThreadLocal記憶體洩露的資料請關注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