一、前言
對一個事務的認知是一個遞進的過程。在了解ThreadLocal時,需要注意以下幾點:
- 什么是ThreadLocal? ThreadLocal出現的背景是什么?解決了什么問題?
- ThreadLocal的使用方法是什么?使用的效果如何?
- ThreadLocal是如何實現它的功能的,即ThreadLocal的原理是什么?
二、背景
??????在一個分布式系統中,多個線程同時訪問同一類實例中的某個變量a,由于變量a是線程共享的,導致一個線程對變量a進行修改,其他線程讀到的都是修改后的變量a的值(如果是存在多個線程同時寫,需要加分布式鎖,限制同時只能一個線程對其進行修改)。這種情況在普通的場景下是合理的,比如在電商系統中,買家點擊支付訂單兩次(兩個獨立的線程),第一次生成訂單,會修改冪等值(防止第二次重復下單),第二次訪問的時候去判斷冪等值,如果已被修改,則不會重新生成訂單。所以線程間變量共享是必須的。
??????但存在這樣一個場景:還是以電商系統為例。買家在訪問訂單詳情頁的時候,在不同的條件下會查訂單(查數據庫)。查庫涉及io,對系統的開銷和響應時間有較大的影響。由于訂單詳情頁的渲染都是一些讀操作,沒有寫操作,所以,需要在查數據庫時做一層本地緩存。而且這個本地緩存是對線程敏感的,只在當前線程生效,別的線程無法訪問這個緩存,也就是說線程間是隔離的。
??????上面只是以電商為例, 所以需要一種方式,能夠實現變量的線程間隔離,此變量只能在當前線程生效,不同的線程變量有不同的值。基于以上訴求,java誕生了ThreadLocal,主要是為了解決內存的線程隔離。
三、使用方式
3.1 測試代碼
- 線程類
public class NormalThread implements Runnable {
private int shareValue = 0;
ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();
@Override
public void run() {
shareValue += 1;
threadLocalValue.set(shareValue);
System.out.println(Thread.currentThread().getName() + "===== shareValue:" + shareValue + " threadLocal:" + threadLocalValue.get());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "===== shareValue:" + shareValue + " threadLocal:" + threadLocalValue.get());
}
}
- 線程池
public class MutiThreadUtil {
private static int core_pool_size = 4;
private static int max_pool_size = 10;
//如果空閑立即退出
private static long keep_alive_time = 0L;
//隊列的容量是0
private static BlockingQueue queue = new SynchronousQueue();
//隊列容量為1
private static ArrayBlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue(1);
public static ExecutorService initThreadPool() {
ExecutorService executorService = new ThreadPoolExecutor(
core_pool_size, max_pool_size, keep_alive_time,TimeUnit.SECONDS,queue
);
return executorService;
}
}
- 主線程
public class ThreadLocalTest {
public static void main(String[] args) {
NormalThread normalThread = new NormalThread();
ExecutorService executorService = MutiThreadUtil.initThreadPool();
for (int i = 0; i < 10; i ++) {
executorService.execute(normalThread);
}
executorService.shutdown();
}
}
3.2 結果分析
pool-1-thread-1===== shareValue:1 threadLocal:1
pool-1-thread-2===== shareValue:2 threadLocal:2
pool-1-thread-3===== shareValue:3 threadLocal:3
pool-1-thread-8===== shareValue:4 threadLocal:4
pool-1-thread-4===== shareValue:5 threadLocal:5
pool-1-thread-5===== shareValue:6 threadLocal:6
pool-1-thread-6===== shareValue:7 threadLocal:7
pool-1-thread-7===== shareValue:8 threadLocal:8
pool-1-thread-9===== shareValue:9 threadLocal:9
pool-1-thread-10===== shareValue:10 threadLocal:10
pool-1-thread-3===== shareValue:10 threadLocal:3
pool-1-thread-2===== shareValue:10 threadLocal:2
pool-1-thread-1===== shareValue:10 threadLocal:1
pool-1-thread-8===== shareValue:10 threadLocal:4
pool-1-thread-7===== shareValue:10 threadLocal:8
pool-1-thread-4===== shareValue:10 threadLocal:5
pool-1-thread-5===== shareValue:10 threadLocal:6
pool-1-thread-9===== shareValue:10 threadLocal:9
pool-1-thread-10===== shareValue:10 threadLocal:10
pool-1-thread-6===== shareValue:10 threadLocal:7
以線程pool-1-thread-1線程(后面簡稱線程1)作為分析,剛開始 線程1的shareValue 和 threadlocal 值均為1, shareValue是共享變量,在線程1 sleep階段,線程2-10均執行了以下代碼
shareValue += 1;
threadLocalValue.set(shareValue);
但從sleep后的打印結果來看,線程1只是更改了shareValue10的值,變為10, 而threadlocal的值沒有變,還是1,這說明threadlocal的值是線程級的,是線程的私有空間,不會因為其他線程的改變而改變。證明了thread的線程隔離性。
四、ThreadLocal 原理
在不看源碼之前,我們思考下如果讓我們設計這樣一個工具類,能夠使得線程間的變量相互隔離,我們會怎樣設計?
??????每一個線程,其執行均是依靠Thread類的實例的start方法來啟動線程,然后CPU來執行線程。每一個Thread類的實例的運行即為一個線程。若要每個線程(每個Thread實例)的變量空間隔離,則需要將這個變量的定義聲明在Thread這個類中。這樣,每個實例都有屬于自己的這個變量的空間,則實現了線程的隔離。事實上,ThreadLocal的源碼也是這樣實現的。
4.1 實現內存線程間隔離的原理
- 在Thread類中聲明一個公共的類變量ThreadLocalMap,用以在Thread的實例中預占空間
ThreadLocal.ThreadLocalMap threadLocals = null;
- 在ThreadLocal中創建一個內部類ThreadLocalMap,這個Map的key是ThreadLoca對象,value是set進去的ThreadLocal中泛型類型的值
private void set(ThreadLocal<?> key, Object value) {...}
- 在new ThreadLocal時,只是簡單的創建了個ThreadLocal對象,與線程還沒有任何關系
- 真正產生關系的是在向ThreadLocal對象中set值得時候:
- 首先從當前的線程中獲取ThreadLocalMap,如果為空,則初始化當前線程的ThreadLocalMap
- 然后將值set到這個Map中去,如果不為空,則說明當前線程之前已經set過ThreadLocal對象了。
這樣用一個ThreadHashMap來存儲當前線程的若干個可以線程間隔離的變量,key是ThreadLocal對象,value是要存儲的值(類型是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);
}
- 從ThreadLocal中獲取值 :還是先從當前線程中獲取ThreadLocalMap,然后使用ThreadLocal對象(key)去獲取這個對象對應的值(value)
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是如何實現的線程級別的隔離已經完全足夠了。簡單的講,就是在Thread的類中聲明了ThreadLocalMap這個類,然后在使用ThreadLocal對象set值的時候將當前線程(Thread實例)進行map初始化,并將Threadlocal對應的值塞進map中,下次get的時候,也是使用這個ThreadLcoal的對象(key)去從當前線程的map中獲取值(value)就可以了
4.2 ThreadLocalMap的深究
從源碼上看,ThreadLocalMap雖然叫做Map,但和我們常規理解的Map不太一樣,因為這個類并沒有實現Map這個接口,只是定義在ThreadLocal中的一個靜態內部類。只是因為在存儲的時候也是以key-value的形式作為方法的入參暴露出去,所以稱為map。
static class ThreadLocalMap {...}
- ThreadLocalMap的創建,在使用ThreadLocal對象set值的時候,會創建ThreadLocalMap的對象,可以看到,入參就是KV,key是ThreadLocal對象,value是一個Entry對象,存儲kv(HashMap是使用Node作為KV對象存儲)。Entry的key是ThreadLocal對象,vaule是set進去的具體值。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 繼續看看在創建ThreadLocalMap實例的時候做了什么?其實ThreadLocalMap存儲是一個Entry類型的數組,key提供了hashcode用來計算存儲的數組地址(散列法解決沖突)
- 創建Entry數組(初始容量16)
- 然后獲取到key(ThreadLocal對象)的hashcode(是一個自增的原子int型)
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
- 使用【hashcode 模(%) 數組長度】的方式得到要將key存儲到數組的哪一位。
- 設置數組的擴容閾值,用以后續擴容
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);
}
創建ThreadLcoalMap對象只有在當前線程第一次插入kv的時候發生,如果是第二次插入kv,則會進行第三步
- 這個set的過程其實就是根據ThreadLocal的hashcode來計算存儲在Entry數組的位置
- 利用ThreadLocal的【hashcode 模(%) 數組長度】的方式獲取存儲在數組的位置
- 如果當前位置已存在值,則向右移一位,如果也存在值,則繼續右移,直到有空位置出現為止
- 將當前的value存儲上面兩部得到的索引位置(上面這兩步就是散列法的實現)
- 校驗是否擴容,如果當前數組的中存儲的值得數量大于閾值(數組長度的2/3),則擴容一倍,并將原來的數組的值重新hash至新數組中(這個過程其實就是HashMap的擴容過程)
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) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
4.3 ThreadLocalMap和HashMap的比較
- 上述的整個過程其實和HashMap的實現方式很相像,相同點:
- 兩個map都是最終用數組作為存儲結構,使用key做索引,value是真正存儲在數組索引上的值。
- 不同點:解決key沖突的方式
- map解決沖突的方式不一樣,HashMap采用鏈表法,ThreadLocalMap采用散列法(又稱開放地址法)
思考:為什么不采用HashMap作為ThreadLocal的存儲結構?
個人理解:
- 引入鏈表,徒增了數據結構的復雜度,并且鏈表的讀取效率較低
- 更加靈活。包括方法的定義和數組的管理,更加適合當前場景
- 不需要HashMap的額外的很多方法和變量,需要一個更加純粹和干凈map,來存儲自己需要的值,減少內存的損耗。
4.4 ThreadLocal的生命周期
ThreadLocal的生命周期和當前Thread的生命周期強綁定
- 正常情況
正常情況下(當然會有非正常情況),在線程退出的時候會將threadLocals這個變量置為null,等待JVM去自動回收。
注意:Thread這個方法只是用以系統能夠顯示的調用退出線程,線程在結束的時候是不會調用這個方法,啟動的線程是非守護線程,會在線程結束的時候由jvm自動進行空間的釋放和回收。
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
- 非正常情況
由于現在多線程一般都是由線程池管理,而線程池的線程一般都是復用的,這樣會導致線程一直存活,而如果使用ThreadLocal大量存儲變量,會使得空間開始膨脹
- 啟發
需要自己來管理ThreadLocal的生命周期,在ThreadLocal使用結束以后及時調用remove()方法進行清理。
五、注意事項
- 注意管理ThreadLocal的聲明周期,及時調用remove方法進行空間釋放
- 注意ThreadLocal的使用方式,如果在使用中發現沒有獲取到預期的值,只能是自己的使用方式不對,導致獲取的不是同一線程下的ThreadLocal值。