本文主要介紹 ThreadLocal、InheritableThreadLocal挨务、TransmittableThreadLocal 三者之間區(qū)別击你、如何使用玉组、什么場景使用以及對原理和源碼的介紹。介紹原理的時候通過最直白丁侄、最易懂的語言爭取讓大家了解三者之間的區(qū)別惯雳,以及日常如何把他們使用起來
ThreadLocal
ThreadLocal 解決的是每個線程可以擁有自己線程的變量實例『枰。可以從隔離的角度解決變量線程安全的問題石景。
舉個例子:
用戶登陸后將用戶的信息保存到 ThreadLocal 中,ThreadLocal<User> 可以保存請求過來的信息拙吉。也就是下面在這一個線程中任何地方都可以訪問到這個 ThreadLocal 中的變量潮孽。雖然這是一個全局的靜態(tài)變量,但是當(dāng)有多個個線程調(diào)用 UserContext.setUser() 方法的時候筷黔,多個線程的變量都會保存往史,多個線程之間不會被相互覆蓋。
看下下面代碼佛舱,在同一個 JVM 進(jìn)程中椎例,雖然只有一個靜態(tài)變量 userHolder,但是
線程A調(diào)用:UserContext.setUser(userA)
同時線程B調(diào)用:UserContext.setUser(userB)
在線程A中調(diào)用 UserContext.getUser() 得到的結(jié)果是 userA
在線程B中調(diào)用 UserContext.getUser() 得到的結(jié)果是 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();
}
}
實現(xiàn)原理:
ThreadLocal 是每個 Thread 都綁定一個 Map名眉,線程之間不會互相干擾
這個Map 不是普通的 Map而是一個定制 Map:ThreadLocalMap粟矿。
這個Map 使用了使用“開放尋址法”中最簡單的“線性探測法”解決散列沖突問題。這點是和我們平常使用的普通的 HashMap 不一樣的损拢。其中還是用了一個神奇的數(shù)字 HASH_INCREMENT= 0x61c88647,保證了一個完美的散列分布撒犀。具體可以參考斐波那契散列法的相關(guān)資料
我們看它的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所以產(chǎn)生了ThreadLocal的進(jìn)化版本
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方法中g(shù)etUser的返回值是為null的映凳。如果把InheritableThreadLocal替換為ThreadLocal胆筒,那么new Thread中的
UserContext.getUser();返回值是NULL
原理
我們看 InheritableThreadLocal 的源碼發(fā)現(xiàn)它并沒有太多的方法,其實主要的代碼是在 Thread 的 init() 方法中诈豌。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
可以看到 inheritableThreadLocals 還是一個 ThreadLocalMap仆救,只不過是在 Thread 的 init 方法中把父Thread的inheritableThreadLocals 變量 copy 了一份給自己。同樣借助 ThreadLocalMap 子線程可以獲取到父線程的所有變量矫渔。
根據(jù)它的實現(xiàn)彤蔽,我們也可以看到它的缺點,就是 Thread的 init 方法是在線程構(gòu)造方法中 copy的庙洼,也就是在線程復(fù)用的線程池中是沒有辦法使用的顿痪。
TransmittableThreadLocal
上面介紹了 ThreadLocal 可以同線程共享變量镊辕。InheritableThreadLocal 可以父子線程共享變量,那么我們經(jīng)常使用的線程池如何使用 ThreadLocal 這樣的功能呢蚁袭?TransmittableThreadLocal 簡稱(TTL)是阿里開源的一款支持線程池的 ThreadLocal 組件征懈。
舉個例子:
TenantContext 的實現(xiàn)是使用的 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
原因如下:例子中使用了固定線程數(shù)量為1的固定線程池揩悄。第一次sumbit task的時候卖哎,線程池會創(chuàng)建線程,因為使用了 InheritableThreadLocal 虏束,所以調(diào)用 Thread 的 init 方法的時候會取父線程的 inheritableThreadLocals. 所以第一打印可以打印出 #########, mzt_1棉饶。下面又使用了 TenantContext.setTenantId("mzt_" + 2); 方法,之后又 submit 了一個線程镇匀,但是這時候線程池里面已經(jīng)又線程了所以不會重新create線程了照藻,也不會調(diào)用 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);
}
重新運行之后的結(jié)果:
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 的時候復(fù)制一份父線程數(shù)據(jù),那么為了支持線程池就需要在 Thread 的 run 方法之前把父線程的數(shù)據(jù) 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 類了,它實現(xiàn)了 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方法之前復(fù)制了父線程的ThreadLocal變量。當(dāng)線程執(zhí)行時官觅,調(diào)用 TtlRunnable run 方法纵菌,TtlRunnable 會從 AtomicReference 中獲取出調(diào)用線程中所有的上下文,并把上下文給 TransmittableThreadLocal.Transmitter.replay 方法把上下文復(fù)制到當(dāng)前線程休涤。并把上下文備份咱圆。
當(dāng)線程執(zhí)行完,調(diào)用 TransmittableThreadLocal.Transmitter.restore 并把備份的上下文傳入功氨,恢復(fù)備份的上下文序苏,把后面新增的上下文刪除,并重新把上下文復(fù)制到當(dāng)前線程疑故。
延伸
我們發(fā)現(xiàn)通過對線程池包裹一層還是侵入性太強杠览,不符合SOLID原則,不優(yōu)雅纵势。TTL也提供了通過agent方式接入的方法踱阿,具體可以在TTL官網(wǎng)看文檔管钳。這里就不介紹了。