javascript
Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失
在Spring Cloud中我們用Hystrix來實現斷路器,Zuul中默認是用信號量(Hystrix默認是線程)來進行隔離的,我們可以通過配置使用線程方式隔離。
在使用線程隔離的時候,有個問題是必須要解決的,那就是在某些業務場景下通過ThreadLocal來在線程里傳遞數據,用信號量是沒問題的,從請求進來,但后續的流程都是通一個線程。
當隔離模式為線程時,Hystrix會將請求放入Hystrix的線程池中去執行,這個時候某個請求就有A線程變成B線程了,ThreadLocal必然消失了。
下面我們通過一個簡單的列子來模擬下這個流程:
| public class CustomThreadLocal { static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { new Thread(new Runnable() { public void run() { CustomThreadLocal.threadLocal.set("猿天地"); new Service().call(); } }).start(); } } class Service { public void call() { System.out.println("Service:" + Thread.currentThread().getName()); System.out.println("Service:" + CustomThreadLocal.threadLocal.get()); new Dao().call(); } } class Dao { public void call() { System.out.println("=========================="); System.out.println("Dao:" + Thread.currentThread().getName()); System.out.println("Dao:" + CustomThreadLocal.threadLocal.get()); } } |
我們在主類中定義了一個ThreadLocal用來傳遞數據,然后起了一個線程,在線程中調用Service中的call方法,并且往Threadlocal中設置了一個值,在Service中獲取ThreadLocal中的值,然后再調用Dao中的call方法,也是獲取ThreadLocal中的值,我們運行下看效果:
| Service:Thread-0 Service:猿天地 ========================== Dao:Thread-0 Dao:猿天地 |
可以看到整個流程都是在同一個線程中執行的,也正確的獲取到了ThreadLocal中的值,這種情況是沒有問題的。
接下來我們改造下程序,進行線程切換,將調用Dao中的call重啟一個線程執行:
| public class CustomThreadLocal { static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { new Thread(new Runnable() { public void run() { CustomThreadLocal.threadLocal.set("猿天地"); new Service().call(); } }).start(); } } class Service { public void call() { System.out.println("Service:" + Thread.currentThread().getName()); System.out.println("Service:" + CustomThreadLocal.threadLocal.get()); //new Dao().call(); new Thread(new Runnable() { public void run() { new Dao().call(); } }).start(); } } class Dao { public void call() { System.out.println("=========================="); System.out.println("Dao:" + Thread.currentThread().getName()); System.out.println("Dao:" + CustomThreadLocal.threadLocal.get()); } } |
再次運行,看效果:
| Service:Thread-0 Service:猿天地 ========================== Dao:Thread-1 Dao:null |
可以看到這次的請求是由2個線程共同完成的,在Service中還是可以拿到ThreadLocal的值,到了Dao中就拿不到了,因為線程已經切換了,這就是開始講的ThreadLocal的數據會丟失的問題。
那么怎么解決這個問題呢,其實也很簡單,只需要改一行代碼即可:
| static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>(); |
將ThreadLocal改成InheritableThreadLocal,我們看下改造之后的效果:
| Service:Thread-0 Service:猿天地 ========================== Dao:Thread-1 Dao:猿天地 |
值可以正常拿到,InheritableThreadLocal就是為了解決這種線程切換導致ThreadLocal拿不到值的問題而產生的。
要理解InheritableThreadLocal的原理,得先理解ThreadLocal的原理,我們稍微簡單的來介紹下ThreadLocal的原理:
- 每個線程都有一個 ThreadLocalMap 類型的 threadLocals 屬性,ThreadLocalMap 類相當于一個Map,key 是 ThreadLocal 本身,value 就是我們設置的值。
| public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; } |
- 當我們通過 threadLocal.set(“猿天地”); 的時候,就是在這個線程中的 threadLocals 屬性中放入一個鍵值對,key 是 當前線程,value 就是你設置的值猿天地。
| 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.get() 方法的時候,就是根據當前線程作為key來獲取這個線程設置的值。
| public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { ("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } |
通過上面的介紹我們可以了解到threadlocal能夠傳遞數據是用Thread.currentThread()當前線程來獲取,也就是只要在相同的線程中就可以獲取到前方設置進去的值。
如果在threadlocal設置完值之后,下步的操作重新創建了一個線程,這個時候Thread.currentThread()就已經變了,那么肯定是拿不到之前設置的值。具體的問題復現可以參考上面我的代碼。
那為什么InheritableThreadLocal就可以呢?
InheritableThreadLocal這個類繼承了ThreadLocal,重寫了3個方法,在當前線程上創建一個新的線程實例Thread時,會把這些線程變量從當前線程傳遞給新的線程實例。
| public class InheritableThreadLocal<T> extends ThreadLocal<T> { /** * Computes the child's initial value for this inheritable thread-local * variable as a function of the parent's value at the time the child * thread is created. This method is called from within the parent * thread before the child is started. * <p> * This method merely returns its input argument, and should be overridden * if a different behavior is desired. * * @param parentValue the parent thread's value * @return the child thread's initial value */ protected T childValue(T parentValue) { return parentValue; } /** * Get the map associated with a ThreadLocal. * * @param t the current thread */ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } /** * Create the map associated with a ThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the table. */ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } } |
通過上面的代碼我們可以看到InheritableThreadLocal 重寫了childValue, getMap,createMap三個方法,當我們往里面set值的時候,值保存到了inheritableThreadLocals里面,而不是之前的threadLocals。
關鍵的點來了,為什么當創建新的線程池,可以獲取到上個線程里的threadLocal中的值呢?原因就是在新創建線程的時候,會把之前線程的inheritableThreadLocals賦值給新線程的inheritableThreadLocals,通過這種方式實現了數據的傳遞。
源碼最開始在Thread的init方法中,如下:
| if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); |
createInheritedMap如下:
| static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); } |
賦值代碼:
| private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { ("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } } |
到此為止,通過inheritableThreadLocals我們可以在父線程創建子線程的時候將Local中的值傳遞給子線程,這個特性已經能夠滿足大部分的需求了,但是還有一個很嚴重的問題是如果是在線程復用的情況下就會出問題,比如線程池中去使用inheritableThreadLocals 進行傳值,因為inheritableThreadLocals 只是會再新創建線程的時候進行傳值,線程復用并不會做這個操作,那么要解決這個問題就得自己去擴展線程類,實現這個功能。
不要忘記我們是做Java的哈,開源的世界有你需要的任何東西,下面我給大家推薦一個實現好了的Java庫,是阿里開源的transmittable-thread-local。
GitHub地址:https://github.com/alibaba/transmittable-thread-local
主要功能就是解決在使用線程池等會緩存線程的組件情況下,提供ThreadLocal值的傳遞功能,解決異步執行時上下文傳遞的問題。
JDK的InheritableThreadLocal類可以完成父線程到子線程的值傳遞。但對于使用線程池等會緩存線程的組件的情況,線程由線程池創建好,并且線程是緩存起來反復使用的;這時父子線程關系的ThreadLocal值傳遞已經沒有意義,應用需要的實際上是把 任務提交給線程池時的ThreadLocal值傳遞到任務執行時。
transmittable-thread-local使用方式分為三種,修飾Runnable和Callable,修飾線程池,Java Agent來修飾JDK線程池實現類
接下來給大家演示下線程池的修飾方式,首先來一個非正常的案例,代碼如下:
| public class CustomThreadLocal { static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>(); static ExecutorService pool = Executors.newFixedThreadPool(2); public static void main(String[] args) { for(int i=0;i<100;i++) { int j = i; pool.execute(new Thread(new Runnable() { public void run() { CustomThreadLocal.threadLocal.set("猿天地"+j); new Service().call(); } })); } } } class Service { public void call() { CustomThreadLocal.pool.execute(new Runnable() { public void run() { new Dao().call(); } }); } } class Dao { public void call() { System.out.println("Dao:" + CustomThreadLocal.threadLocal.get()); } } |
運行上面的代碼出現的結果是不正確的,輸出結果如下:
| Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 Dao:猿天地99 |
正確的應該是從1到100,由于線程的復用,值被替換掉了才會出現不正確的結果
接下來使用transmittable-thread-local來改造有問題的代碼,添加transmittable-thread-local的Maven依賴:
| <dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.2.0</version> </dependency> |
只需要修改2個地方,修飾線程池和替換InheritableThreadLocal:
| static TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal<>(); static ExecutorService pool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2)); |
正確的結果如下:
| Dao:猿天地85 Dao:猿天地84 Dao:猿天地86 Dao:猿天地87 Dao:猿天地88 Dao:猿天地90 Dao:猿天地89 Dao:猿天地91 Dao:猿天地93 Dao:猿天地92 Dao:猿天地94 Dao:猿天地95 Dao:猿天地97 Dao:猿天地96 Dao:猿天地98 Dao:猿天地99 |
到這里我們就已經可以完美的解決線程中,線程池中ThreadLocal數據的傳遞了,各位看官又疑惑了,標題不是講的Spring Cloud中如何解決這個問題么,我也是在Zuul中發現這個問題的,解決方案已經告訴大家了,至于怎么解決Zuul中的這個問題就需要大家自己去思考了,后面有時間我再分享給大家。
總結
以上是生活随笔為你收集整理的Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: React Native在美团外卖客户端
- 下一篇: 工作中如何做好技术积累