目錄
- ThreadLocal
- 舉個例子
- 原理
- InheritableThreadLocal
- 舉個例子
- 原理
- TransmittableThreadLocal
- 舉個例子
- 原理
- 延伸
- 參考:
本文主要介紹ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal三者之間區別、如何使用、什么場景使用以及對原理和原始碼的介紹,介紹原理的時候通過最直白、最易懂的語言爭取讓大家了解三者之間的區別,以及日常如何把他們使用起來
ThreadLocal
ThreadLocal解決的是每個執行緒可以擁有自己執行緒的變數實體,可以從隔離的角度解決變數執行緒安全的問題,
舉個例子
用戶登陸后將用戶的資訊保存到ThreadLocal中,ThreadLocal 可以保存請求過來的資訊,也就是下面在這一個執行緒中任何地方都可以訪問到這個ThreadLocal中的變數,雖然這是一個全域的靜態變數,但是當有多個個執行緒呼叫UserContext.setUser()方法的時候,多個執行緒的變數都會保存,多個執行緒之間不會被相互覆寫,
看下下面代碼,在同一個JVM行程中,雖然只有一個靜態變數userHolder,但是
執行緒A呼叫:UserContext.setUser(userA)
同時執行緒B呼叫:UserContext.setUser(userB)
在執行緒A中呼叫UserContext.getUser()得到的結果是userA
在執行緒B中呼叫UserContext.getUser()得到的結果是userB
public class UserContext {
private UserContext() {
}
private static ThreadLocal<User> userHolder = new ThreadLocal<>();
public static User getUser() {
return userHolder.get();
}
public static void setUser(User user) {
userHolder.set(user);
}
public static void clean() {
userHolder.remove();
}
}
原理
ThreadLocal是每個Thread都系結一個Map,執行緒之間不會互相干擾
這個Map不是普通的Map而是一個定制Map:ThreadLocalMap,
這個Map使用了使用“開放尋址法”中最簡單的“線性探測法”解決散列沖突問題,這點是和我們平常使用的普通的HashMap不一樣的,其中還是用了一個神奇的數字HASH_INCREMENT= 0x61c88647,保證了一個完美的散列分布,具體可以參考斐波那契散列法的相關資料
我們看它的get原始碼:
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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原始碼的前兩行可以看到,ThreadLocal的缺點:它不支持子執行緒,因為map是系結在currentThread中的,子執行緒和父執行緒并不是一個Thread所以產生了ThreadLocal的進化版本
InheritableThreadLocal
前面說到ThreadLocal并不支持子執行緒,InheritableThreadLocal就是支持子執行緒的ThreadLocal
舉個例子
public class UserContext {
private UserContext() {
}
private static InheritableThreadLocal<User> userHolder = new InheritableThreadLocal<>();
public static User getUser() {
return userHolder.get();
}
public static void setUser(User user) {
userHolder.set(user);
}
public static void clean() {
userHolder.remove();
}
}
@Test
public void test3() throws InterruptedException {
UserEntity userEntity = new UserEntity();
userEntity.setUsername("mzt");
UserContext.setUser(userEntity);
UserEntity user = UserContext.getUser();
Assert.assertNotNull(user);
Assert.assertEquals(user.getUsername(), "mzt");
new Thread(() -> {
final UserEntity user1 = UserContext.getUser();
Assert.assertNotNull(user1);
Assert.assertEquals(user1.getUsername(), "mzt");
}).start();
TimeUnit.MINUTES.sleep(1);
}
因為使用了InheritableThreadLocal這時候兩個Assert都是正確的,但是僅僅使用ThreadLocal的時候上面例子中new Threa的run方法中getUser的回傳值是為null的,如果把InheritableThreadLocal替換為ThreadLocal,那么new Thread中的
UserContext.getUser();回傳值是NULL
原理
我們看InheritableThreadLocal的原始碼發現它并沒有太多的方法,其實主要的代碼是在Thread的init()方法中,
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
可以看到inheritableThreadLocals還是一個ThreadLocalMap,只不過是在Thread的init方法中把父Thread的inheritableThreadLocals變數copy了一份給自己,同樣借助ThreadLocalMap子執行緒可以獲取到父執行緒的所有變數,
根據它的實作,我們也可以看到它的缺點,就是Thread的init方法是在執行緒構造方法中copy的,也就是在執行緒復用的執行緒池中是沒有辦法使用的,
TransmittableThreadLocal
上面介紹了ThreadLocal可以同執行緒共享變數,InheritableThreadLocal可以父子執行緒共享變數,那么我們經常使用的執行緒池如何使用ThreadLocal這樣的功能呢?TransmittableThreadLocal簡稱(TTL)是阿里開源的一款支持執行緒池的ThreadLocal組件,
舉個例子
TenantContext的實作是使用的InheritableThreadLocal
public class TenantContext {
private static InheritableThreadLocal<String> tenantHolder = new InheritableThreadLocal<>();
// 其他方法略....
}
@Test
public void test1() throws InterruptedException {
ExecutorService ttlExecutorService = Executors.newFixedThreadPool(1);
TenantContext.setTenantId("mzt_" + 1);
ttlExecutorService.submit(() -> {
String tenantId = TenantContext.getTenantId();
log.info("#########, {}", tenantId);
//TenantContext.clean();
});
TimeUnit.SECONDS.sleep(2);
TenantContext.setTenantId("mzt_" + 2);
ttlExecutorService.submit(() -> {
log.info("#########, {}", TenantContext.getTenantId());
// TenantContext.clean();
});
Thread.sleep(2000L);
}
上面的例子中兩次輸出都是什么呢?
2021/01/24 21:49:24.678 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:77) #########, mzt_1
2021/01/24 21:49:26.671 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:83) #########, mzt_1
原因如下:例子中使用了固定執行緒數量為1的固定執行緒池,第一次sumbit task的時候,執行緒池會創建執行緒,因為使用了InheritableThreadLocal,所以呼叫Thread的init方法的時候會取父執行緒的inheritableThreadLocals. 所以第一列印可以列印出 #########, mzt_1,下面又使用了 TenantContext.setTenantId(“mzt_” + 2);方法,之后又submit了一個執行緒,但是這時候執行緒池里面已經又執行緒了所以不會重新create執行緒了,也不會呼叫thread的init方法了,所以get出來的變數還是mzt1 ,
那么下面改成阿里的TTL
public class TenantContext {
private static TransmittableThreadLocal<String> tenantHolder = new TransmittableThreadLocal<>();
// 其他方法略....
}
@Test
public void test1() throws InterruptedException {
ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
TenantContext.setTenantId("mzt_" + 1);
ttlExecutorService.submit(() -> {
String tenantId = TenantContext.getTenantId();
log.info("#########, {}", tenantId);
//TenantContext.clean();
});
TimeUnit.SECONDS.sleep(2);
TenantContext.setTenantId("mzt_" + 2);
ttlExecutorService.submit(() -> {
log.info("#########, {}", TenantContext.getTenantId());
// TenantContext.clean();
});
Thread.sleep(2000L);
}
重新運行之后的結果:
2021/01/24 21:58:58.751 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:77) #########, mzt_1
2021/01/24 21:59:00.746 pool-3-thread-1 [INFO] TenantContextTest (TenantContextTest.java:83) #########, mzt_2
原理
我們看到普通的執行緒池子被TtlExecutors.getTtlExecutorService()包裹了一下,如果僅僅是吧InheritableThreadLocal修改為TransmittableThreadLocal是不起作用的,
所以TTL的做法也比較直接,使用了裝飾器模式,既然InheritableThreadLocal只是在執行緒Create的時候復制一份父執行緒資料,那么為了支持執行緒池就需要在Thread的run方法之前把父執行緒的資料copy一下就可以了,從原始碼中看是
public static ExecutorService getTtlExecutorService(ExecutorService executorService) {
if (executorService == null || executorService instanceof ExecutorServiceTtlWrapper) {
return executorService;
}
return new ExecutorServiceTtlWrapper(executorService);
}
重點:ExecutorServiceTtlWrapper包裝了我們普通的ExecutorService,然后充血了submit方法
@Override
public <T> Future<T> submit(Runnable task, T result) {
return executorService.submit(TtlRunnable.get(task), result);
}
重點就是TtlRunnable類了,它實作了Runable方法,并且重寫了run方法
/**
* wrap method {@link Runnable#run()}.
*/
@Override
public void run() {
Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
try {
runnable.run();
} finally {
TransmittableThreadLocal.restoreBackup(backup);
}
}
這個代碼就是流程的核心了,就是在run方法之前復制了父執行緒的ThreadLocal變數,當執行緒執行時,呼叫 TtlRunnable run 方法,TtlRunnable 會從 AtomicReference 中獲取出呼叫執行緒中所有的背景關系,并把背景關系給 TransmittableThreadLocal.Transmitter.replay 方法把背景關系復制到當前執行緒,并把背景關系備份,
當執行緒執行完,呼叫 TransmittableThreadLocal.Transmitter.restore 并把備份的背景關系傳入,恢復備份的背景關系,把后面新增的背景關系洗掉,并重新把背景關系復制到當前執行緒,
延伸
我們發現通過對執行緒池包裹一層還是侵入性太強,不符合SOLID原則,不優雅,TTL也提供了通過agent方式接入的方法,具體可以在TTL官網看檔案,這里就不介紹了,
參考:
https://www.cnblogs.com/dennyzhangdd/p/7978455.html
https://www.javaspecialists.eu/archive/Issue164-Why-0x61c88647.html
https://blog.xieyangzhe.com/archives/45
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/260569.html
標籤:java
