ThreadLocal Java多線程下的影分身之術

      網友投稿 819 2025-03-31

      如果寫過多線程的代碼,你肯定考慮過線程安全問題,更進一步你可能還考慮在在線程安全的前提下性能的問題。大多數情況下大家用來解決線程安全問題都會使用同步,比如用synchron或者concurrent包提供的各種鎖,當然這些都能解決問題。但有多線程做同步一定會涉及到資源爭搶和等待的問題。java中各種同步方法都是提供一種準入機制,JVM會調用系統同步原語來保證臨界區任意時刻只能有一個線程進入,那必然其他線程都得等待了,性能的瓶頸就在這同步上了。

      解決問題最好的方式是啥?當然是避免問題的發生了。ThreadLocal就是用這樣一種方式提升性能的。ThreadLocal遍歷會為每個線程單獨維護一份值,某個線程對其做任何操作都不會影響其他的線程,這相當于這個對象在每個線程下面都有了一個分身。ThreadLocal是以Thread為維度實現的,所以多線程之間也不會有爭搶和等待,從而避免同步變成瓶頸,下文我們會從源碼的維度去看這些都是如何實現的。

      ThreadLocal也不是萬金油,它也只能在多線程之間數據相互獨立的情況下使用,如果是多線程間的數據同步,還得使用某個同步的方式。 我的理解,ThreadLocal是在臨時變量完全不共享和全部變量完全共享之間取了個折中,在多線程數據一致的情況下完美的避免了資源爭搶和等待,提高了性能。

      如何使用

      ThreadLocal的使用也很簡單,直接new ThreadLocal(); 就可以了,然后可以通過set()和get()分別設值和獲取值。以下代碼展示我如果使用ThreadLocal如何為每個線程單獨維護一個值的,而且線程之間也不會相互干擾。

      public class Demo extends Thread { private static ThreadLocal counter = new ThreadLocal<>(); @Override public void run() { counter.set(ThreadLocalRandom.current().nextInt(100)); System.out.println(Thread.currentThread().getName() + ":" + counter.get()); try { Thread.sleep(2000 + ThreadLocalRandom.current().nextInt(3000)); } catch (Exception e) { e.printStackTrace(); } counter.set(counter.get()+100); System.out.println(Thread.currentThread().getName() + ":" + counter.get()); } public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Demo(); thread.setName("Thread" + i); thread.start(); } } }

      上面我用到了ThreadLocal的set和get方法,其運行結果如下,因為使用了隨機數,可能每次運行解決會不一致。可以很明顯看得出,雖然多線對統一個Object操作,但卻沒有影響到各自的值。

      Thread1:42 Thread0:20 Thread2:18 Thread3:6 Thread4:76 Thread5:50 Thread6:81 Thread7:75 Thread8:48 Thread9:56 Thread4:176 Thread7:175 Thread1:142 Thread6:181 Thread2:118 Thread5:150 Thread0:120 Thread3:106 Thread9:156

      除了set和get接口外,ThreadLocal還提供了remove(),該方法可以將當前線程的所有內容清除掉。另外還有一個ThreadLocal withInitial()。

      源碼分析

      接下來我們就從源碼來剖析下ThreadLocal是如何實現不同線程下不同值的,首先我們來看下set()方法,這是我們在除了構造函數外第一個用的方法,它也承擔著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); } } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

      set()也非常簡單,我順便也把set()涉及到的兩個方法貼上。set()首先獲取當前線程t,然后從t中獲取ThreadLocalMap,如果ThreadLocalMap為空就創建一個。ThreadLocalMap是ThreadLocal中比較核心的東西,稍后會詳細介紹。上面代碼很顯然,ThreadLocalMap是將ThreadLocal作為map的key。雖然多線程下都是用同一個ThreadLocal對象作為Key的,但每次獲取key對應的Value是從不同的Map中獲取,

      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(); }

      雖然多線程下都是用同一個ThreadLocal對象作為Key的,但每次獲取key對應的Value是從不同的Map中獲取,這就保證了多下次下value不會沖突。get方法在ThreadLocalMap未創建的情況下,還會調用setInitialValue()。

      /** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ 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); } if (this instanceof TerminatingThreadLocal) { TerminatingThreadLocal.register((TerminatingThreadLocal) this); } return value; }

      我總結看Java代碼的方法,就是先看類的聲明,然后按實際用途從每個方法入手看是怎么執行的。

      static class ThreadLocalMap { } ```     ThreadLocalMap是直接聲明在ThreadLocal內部的,其他地方就沒法用了(其實外部也沒必要用,輪map的功能,它實現也沒有HashMap和Tree好)。另外,它沒有實現Map接口,emmm 這就意味它不是一個標準的map了。 ```java static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }

      ThreadLocalMap的Entry繼承了WeakReference,這讓我想到了WeakHashMap,這里用WeakReference的原因也很明確,就是想讓Key在失效后,Map能主動清理相關的Entry。

      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); } private void setThreshold(int len) { threshold = len * 2 / 3; }

      ThreadLocalMap也有幾個默認參數,初始容量INITIAL_CAPACITY,threshold是容量的2/3,就是如果Map中的Entry數量超過總容量的2/3,ThreadLocalMap對進行擴容。

      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); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

      從set方法中我們就可以看出ThreadLocalMap和HashMap,TreeMap的設計不同之處。首先也是對Key求hash值做定位,但當遇到hash沖突的時候,它的選擇不是開鏈,而是調用nextIndex往后移動,直到遇見某個entry為null或者其key和要插入的key一樣。同時,插入的過程也會調用replaceStaleEntry對Map做清理,清理過程比較復雜,我們稍后說。插入后,如果size大于閥值,也會對整個map做擴容操作。

      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); } 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處理key沖突的方式是往后移,直到有空閑的位置。這樣雖然實現簡單,但查的時候問題就來了,根據hash值算出來的位置沒有,并不意味著整個map里沒有,所以得往后遍歷,直到找到或者遍歷到某個空Entry。如果你仔細想想可能就會發現問題,如果只是遍歷到遇到null,而不是遍歷整個tab,可能會漏掉。比如下面這個例子。

      | 0 | 1 | 2 | 3 | 5 | 6 | 7 | | | a | b | c | d | e | |

      開始的時候,tab狀態是這樣的,現在我要插入一個h,其hashcode恰好是1,然而a已經在那了,按插入邏輯,h只能插到7的位置了,插入后如下。

      | 0 | 1 | 2 | 3 | 5 | 6 | 7 | | | a | b | c | d | e | h |

      后來,我把c刪掉,變成了下面這樣。如果我現在想查h,按照上面getEntry的邏輯,是不是遍歷到3就停了,所以找不到h了? getEntry的邏輯表面確實是這樣,但實際上getEntryAfterMiss、remove、gets時都會直接或者間接調用expungeStaleEntry會對表里的數據做整理。expungeStaleEntry()除了利用弱引用的特性對tab中Entry做清理外,還會對之前Hash沖突導致后移的Entry重新安放位置。所以不可能出現下面這種tab排放的。

      | 0 | 1 | 2 | 3 | 5 | 6 | 7 | | | a | b | | d | e | h |

      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; }

      還有set中調用的replaceStaleEntry(),代碼很長,其實也是保證key失效的Entry被清理,Hash沖突的key能放回正確的位置。

      private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }

      ThreadLocal Java多線程下的影分身之術

      看這么多復雜的代碼,最后看個簡單的resize(),ThreadLocalMap的resize相較于HashMap的簡單多了,就是新建一個長度為當前2倍的tab,然后把當前tab中的每個entry重新計算index再插入新tab。

      private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (Entry e : oldTab) { if (e != null) { ThreadLocal k = e.get(); if (k == null) { e.value = null; // Help the GC } 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; }

      看來看去,ThreadLocalMap想要實現的功能和WeakHashMap類似,為什么不直接使用WeakHashMap呢!!

      使用場景

      數據庫連接

      Cache

      線程池

      參考資料

      ThreadLocal源碼

      簡單理解ThreadLocal原理和適用場景,多數據源下ThreadLocal的應用

      Java 任務調度 多線程 通用安全

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:Excel怎么合并單元格? | 詳細教程和技巧
      下一篇:如何在兩張紙的兩列中查找重復或唯一值?
      相關文章
      无码专区一va亚洲v专区在线| 亚洲精品无码永久中文字幕| 国产亚洲人成A在线V网站 | 久久精品国产精品亚洲人人| 亚洲s码欧洲m码吹潮| 亚洲粉嫩美白在线| 亚洲成a人片在线不卡| 久久精品国产亚洲av麻豆图片 | 亚洲高清无码在线观看| 国产精品亚洲一区二区三区久久 | 亚洲欧美中文日韩视频| 亚洲偷自拍另类图片二区| 中文字幕精品三区无码亚洲 | 国产.亚洲.欧洲在线| 亚洲情A成黄在线观看动漫软件 | 亚洲国产精品自在拍在线播放| va亚洲va日韩不卡在线观看| 少妇亚洲免费精品| 亚洲阿v天堂在线2017免费| 亚洲国产精品一区二区三区久久| 亚洲国产主播精品极品网红| 亚洲男人天堂2020| 亚洲情综合五月天| 久久久亚洲精品视频| 91在线亚洲精品专区| 亚洲18在线天美| 亚洲中文无码亚洲人成影院| 亚洲狠狠婷婷综合久久蜜芽| 色婷婷亚洲一区二区三区| 亚洲国产中文字幕在线观看| 亚洲人成网站在线播放vr| 亚洲AV日韩AV永久无码下载| 亚洲精品韩国美女在线| 亚洲综合色区中文字幕| 亚洲丁香婷婷综合久久| 亚洲日韩在线第一页| 国产成人亚洲综合无码精品| 久久夜色精品国产噜噜噜亚洲AV| 亚洲春色在线观看| 亚洲精品无码专区在线播放| 亚洲不卡AV影片在线播放|