100%保證線程安全,還有這種黑科技?
本文轉(zhuǎn)載自微信公眾號(hào)「Java技術(shù)指北」,作者指北君。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java技術(shù)指北公眾號(hào)。
大家好,我是指北君。
今天學(xué)習(xí)了ThreadLocal相關(guān)的知識(shí),發(fā)現(xiàn)原來(lái)道哥(Doug Lea)也用ThreadLocal。既然大師們都喜歡用的,我們必須得研究起來(lái)。大師的背影總是需要追隨。
那么指北君給大家安排上了,如果你擁有了Java中的ThreadLocal,那麼你可以創(chuàng)建一個(gè)只允許同一個(gè)線程讀寫(xiě)的變量。因此,即使兩個(gè)線程執(zhí)行了相同的代碼,并且引用了相同的ThreadLocal變量,這兩個(gè)線程也無(wú)法看到彼此的ThreadLocal。可以說(shuō)ThreadLocal提供了一種代碼線程安全的的簡(jiǎn)單方法。
下面我們就來(lái)看看道哥都用的ThreadLocal。
1 ThreadLocal你來(lái)自哪里
- Since: 1.2
- Author: Josh Bloch and Doug Lea
又是并發(fā)大佬們的杰作,膜拜一下。怪不得道哥也愛(ài)用,自己設(shè)計(jì)的類(lèi)總得用用。下面來(lái)看看基本內(nèi)容與用法吧。
2 ThreadLocal原理
首先請(qǐng)看男神們的介紹
“This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).”
“此類(lèi)提供了thread-local變量。這些變量不同于普通的類(lèi)似變量,因?yàn)樵L問(wèn)某個(gè)變量(通過(guò)其 get 或 set 方法)的每個(gè)線程都有自有的,獨(dú)立初始化的變量副本,ThreadLocal實(shí)例通常是希望將狀態(tài)與線程(例如,用戶(hù)ID或事務(wù)ID)關(guān)聯(lián)的類(lèi)中的私有靜態(tài)字段。”
通過(guò)老爺子們的描述,指北君大概也知道了ThreadLocal的推薦使用場(chǎng)景,
- ThreadLocal提供了一種訪問(wèn)某個(gè)特有變量的方法 訪問(wèn)到的變量屬于當(dāng)前線程,同一線程在任何地方都能訪問(wèn)同一個(gè)線程特有變量。
- 推薦定義為 private static 類(lèi)型,但是Doug Lea老爺子在ThreadLocalRandom 和 ReentrantReadWriteLock 中使用了 private static final 類(lèi)型。(肯定是當(dāng)年寫(xiě)簡(jiǎn)介的時(shí)候手抖了)
2.1 Thread中如何存儲(chǔ)
既然是線程的變量,自然是存在Thread對(duì)象中的一個(gè)變量了,但是它是通過(guò)ThreadLocal這個(gè)類(lèi)來(lái)維護(hù)的。
- //與此線程相關(guān)的ThreadLocal值,由ThreadLocal這個(gè)類(lèi)維護(hù)
- ThreadLocal.ThreadLocalMap threadLocals = null;
- //與此線程相關(guān)的可繼承的ThreadLocal值,由InheritableThreadLocal類(lèi)來(lái)維護(hù)
- ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
ThreadLocal中有一個(gè)內(nèi)部類(lèi)來(lái)ThreadLocalMap來(lái)維護(hù)這些線程本地變量,
- static class ThreadLocalMap {
- //初始容量,2的n次方
- private static final int INITIAL_CAPACITY = 16;
- //根據(jù)需要調(diào)整數(shù)組大小,2的n次方
- private Entry[] table;
- //上面Entry數(shù)組中的元素?cái)?shù)量
- private int size = 0;
- //The next size value at which to resize Default to 0
- private int threshold;
- }
ThreadLocalMap中的Entry結(jié)構(gòu)如下,是一種key為弱引用(其目的就是Entry對(duì)象在GC時(shí)容易回收)的hash map,其中key總是ThreadLocal。
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
2.2 常用方法 get,set,remove 詳解
get() 此方法是ThreadLocal最重要的方法之一,該方法返回此線程局部變量的當(dāng)前線程副本中的值。大概可分為以下幾步:
(1) 先獲取當(dāng)前線程,然后再?gòu)木€程中得到ThreadLocalMap。
(2) 然后使用ThreadLocal對(duì)象的threadLocalHashCode進(jìn)行散列計(jì)算,得到一個(gè)數(shù)組的index
(3) 從Table數(shù)組中得到Entry,再對(duì)比Entry的key是不是和當(dāng)前的ThreadLocal相等,如果相等就返回此Entry的value
(4) 如果上一步中得到的Entry與當(dāng)前ThreadLocal不相等,則會(huì)在方法getEntryAfterMiss中進(jìn)行遍歷Entry數(shù)組table中的每一個(gè)元素,如果找不到就返回null。而且在遍歷的過(guò)程中會(huì)順便清理一下廢棄的Entry。
下面可以看一下get方法的具體代碼。
- public T get() {
- //獲取當(dāng)前線程
- Thread t = Thread.currentThread();
- //從當(dāng)前線程中獲取ThreadLocalMap
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- //獲取map中當(dāng)前ThreadLocal對(duì)象對(duì)應(yīng)的entry
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- T result = (T)e.value;
- return result;
- }
- }
- return setInitialValue();
- }
- private Entry getEntry(ThreadLocal<?> key) {
- //散列計(jì)算得到Entry中當(dāng)前的index
- int i = key.threadLocalHashCode & (table.length - 1);
- Entry e = table[i];
- //如果Entry不是null而且key等于當(dāng)前
- // ThreadLocal對(duì)象則返回此Entry
- if (e != null && e.get() == key)
- return e;
- else
- //Entry==null 或者其key不等于當(dāng)前
- // ThreadLocal對(duì)象,遍歷其余Entry
- 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)
- //如果遍歷過(guò)程中發(fā)現(xiàn)有Entry的Key為Null,
- // 則清除掉作廢的Entry
- expungeStaleEntry(i);
- else
- //計(jì)算Entry數(shù)組下一個(gè)index
- i = nextIndex(i, len);
- e = tab[i];
- }
- return null;
- }
- set(T value) 此方法將此線程局部變量的當(dāng)前線程副本中的值設(shè)置為指定值。
set線程本地變量步驟如下:
(1) 首先依然是獲取此線程的ThreadLocalMap
(2) Map不為null時(shí)往map中插入數(shù)據(jù),否側(cè)創(chuàng)建map并插入數(shù)據(jù)
(3) 具體的set方法依然是先遍歷Entry數(shù)組中所有的的Entry,然后依次對(duì)比每個(gè)Entry的key是否等于當(dāng)前ThreadLocal,如果相等則直接替換現(xiàn)有Entry的value。如果Entry的Key為null,則立馬清理廢棄的Entry,并用新的Entry來(lái)替換此卡槽。
(4) 如果遍歷完都沒(méi)有return,則在在table中相應(yīng)卡槽下新建Entry對(duì)象
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
- 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();
- //如果原Entry的key就是當(dāng)前ThreadLocal對(duì)象,
- // 則直接替換現(xiàn)有value
- if (k == key) {
- e.value = value;
- return;
- }
- if (k == null) {
- // 如果Entry的Key為null, 則直接替換為新的Entry
- replaceStaleEntry(key, value, i);
- return;
- }
- }
- // 如果前面的遍歷沒(méi)有return,
- // 則插入新的Entry對(duì)象到對(duì)應(yīng)的卡槽
- tab[i] = new Entry(key, value);
- int sz = ++size;
- if (!cleanSomeSlots(i, sz) && sz >= threshold)
- rehash();
- }
remove() remove則相對(duì)簡(jiǎn)單,直接遍歷ThreadLocalMap中Entry數(shù)組table,找到對(duì)應(yīng)的Entry,將Entry的key置為null,然后再清理相應(yīng)的Entry。
- private void remove(ThreadLocal<?> key) {
- ...
- for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
- if (e.get() == key) {
- //Entry 的key置為null
- e.clear();
- // 清理對(duì)應(yīng)卡槽,
- expungeStaleEntry(i);
- return;
- }
- }
- }
3 Java中使用的ThreadLocal
Java中有哪些源碼使用了ThreadLocal。
ThreadLocalRandom 中使用計(jì)算nextGaussian值時(shí)有使用到ThreadLocal。
InheritableThreadLocal繼承了ThreadLocal,線程中使用inheritableThreadLocals這個(gè)map存儲(chǔ)線程本地變量。和ThreadLocal的區(qū)別就是子線程依然可以訪問(wèn)到父線程的線程本地變量,實(shí)際應(yīng)用中也推薦InheritableThreadLocal
ReentrantReadWriteLock中線程讀寫(xiě)鎖的計(jì)數(shù)器使用了ThreadLocal,其目的是記錄每個(gè)線程獲取讀寫(xiě)鎖的次數(shù)
- static final class ThreadLocalHoldCounter
- extends ThreadLocal<HoldCounter> {
- public HoldCounter initialValue() {
- return new HoldCounter();
- }
- }
- //曾經(jīng)的Doug Lea老爺子推薦static field,
- // 而他默默的使用了static final。
4 如何使用ThreadLocal
ThreadLocal非常適合存儲(chǔ)非線程安全的對(duì)象,并且不需要跨線程共享對(duì)象。很多需要線程隔離的操作都可以嘗試使用它。
ThreadLocal也非常適合在Web應(yīng)用程序中使用,典型的應(yīng)用就是在Web請(qǐng)求進(jìn)來(lái)一開(kāi)始就將請(qǐng)求狀態(tài)存儲(chǔ)在ThreadLocal中,然后參與處理的任何組件均可訪問(wèn)該狀態(tài)。
以下是一個(gè)ThreadLocal示例:
具體使用就是配合interceptor或者filter在線程剛開(kāi)始執(zhí)行的時(shí)候存儲(chǔ)SessionContext,線程執(zhí)行過(guò)程中可以隨時(shí)訪問(wèn)該變量。然后在線程執(zhí)行結(jié)束的時(shí)候再調(diào)用remove()方法移除,防止內(nèi)存泄漏。
- public class SessionContextHolder {
- private static final ThreadLocal<SessionContex> CONTEXHOLDER
- = new InheritableThreadLocal<>();
- public static void remove(){CONTEXHOLDER.remove();};
- public static SessionContex get(){return CONTEXHOLDER.get();}
- public static void set(SessionContex sessionContex) {CONTEXHOLDER.set(sessionContex);}
- }
總結(jié)
本文介紹了ThreadLocal的原理以及解析了常用方法的實(shí)現(xiàn)邏輯,以及在ThreadLocal一些應(yīng)用。在一步步梳理的過(guò)程中,果然看到了以往忽略的各種細(xì)節(jié),最后給出了一個(gè)小Case。并發(fā)編程大神道哥.李都在用的ThreadLocal,不妨在自己的項(xiàng)目中偷偷用上,保證絲滑舒適。