雖然使用AOP可以獲取方法簽名,但是如果要獲取方法中計算得出的數據,那么就得使用ThreadLocal,如果還涉及父線程,那么可以選擇InheritableThreadLocal.
注意:理解一些原理能夠減少很多不可控問題,最簡單的使用方式就是不要交給線程池處理.為了提高一點性能,而導致數據錯誤得不償失.
2018年4月12日 12:44:41更新 關於InheritableThreadLocal 配合線程池的問題解決方案 -> TransmittableThreadLocal 解決 線程池線程復用 無法復制 InheritableThreadLocal 的問題.
首先看看ThreadLoacl如何做到共享變量實現為線程私有變量
Thread源碼里面,有一個ThreadLoaclMap
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLoacl set方法源碼
public void set(T value) {
//獲取當前線程 Thread t = Thread.currentThread();
//獲取當前線程ThreadLoaclMap ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
ThreadLoacl getMap方法源碼
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
測試TreadLocal線程私有
public class A { static final ThreadLocal<String> threadParam = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { //死循環,測多幾次看結果 while (true) { //線程1 new Thread(() -> { //設置參數 threadParam.set("abc"); //輸出參數 System.out.println("t1:" + threadParam.get()); //看起來像是多余操作 // threadParam.remove(); }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { //線程二,測試是否能獲取abc System.out.println("t2:" + threadParam.get()); }).start(); } } }
測試結果
線程1永遠輸出abc
線程2永遠輸出null
看起來很美好.但是也有需要注意的地方
如果使用線程池,以下把線程交給線程池處理
/** * * @author ZhenWeiLai * */ public class B { static final ThreadLocal<String> threadParam = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { //固定池內只有存活3個線程 ExecutorService execService = Executors.newFixedThreadPool(3); //死循環幾次才能看出效果 while (true) { Thread t = new Thread(()->{ threadParam.set("abc"); System.out.println("t1:" + threadParam.get()); //如果不調用remove,將引發問題 // threadParam.remove(); }); execService.execute(t); TimeUnit.SECONDS.sleep(1); Thread t2 = new Thread(()-> { System.out.println("t2:" + threadParam.get()); }); execService.execute(t2); } } }
測試結果:
t1:abc
t1:abc
t2:null
t2:abc //因復用線程而導致問題
t1:abc
原因:線程池把線程提交到隊列,當被調用的時候如果存在空閑線程就直接復用線程,僅僅是調用了用戶提交的run方法.
所以當ThreadLocal參數使用完,記得調用remove方法
除了ThreadLocal 還有 InheritableThreadLocal,子線程可以共享父線程的InheritableThreadLocal
InheritableThreadLocal實現的關鍵源碼
//初始化一個線程時,獲取當前線程,作為父線程 Thread parent = currentThread(); //如果父線程inheritableThreadLocals 不為空時,子線程復制一份inheritableThreadLocals if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
測試代碼
/** * * @author ZhenWeiLai * */ public class A { static final InheritableThreadLocal<String> threadParam = new InheritableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { //死循環,測多幾次看結果 while (true) { //線程1,測試是否能獲取父線程參數 new Thread(() -> { //設置參數 threadParam.set("abc"); //輸出參數 System.out.println("t1:" + threadParam.get()); //線程2,測試是否能獲取線程1參數 new Thread(() -> { System.out.println("t2:" + threadParam.get()); //為了測試線程三能否獲得,這里先不刪除 // threadParam.remove(); }).start(); }).start(); TimeUnit.SECONDS.sleep(1); //線程3,測試是否能獲取線程1參數 new Thread(() -> { System.out.println("t3:" + threadParam.get()); }).start(); } } }
輸出結果:自線程可以獲取參數,非自線程不能獲取.
t1:abc
t2:abc
t1:abc
t3:null
t2:abc
t3:null
t1:abc
t2:abc
t3:null
t1:abc
t2:abc
再一次看似很美好,以下寫一個復雜點的,交給線程池執行
package thread.base.threadloacl; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * * @author ZhenWeiLai * */ public class B { static final InheritableThreadLocal<String> threadParam = new InheritableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { //固定池內只有存活3個線程 ExecutorService execService = Executors.newFixedThreadPool(3); //死循環幾次才能看出效果 while (true) { //線程1,里面有兩個子線程 Thread t = new Thread(()->{ threadParam.set("abc"); System.out.println("t1:" + threadParam.get()); Thread t2 = new Thread(()->{ System.out.println("t2:" + threadParam.get()); // threadParam.remove(); }); execService.execute(t2); Thread t3 = new Thread(()->{ System.out.println("t3:" + threadParam.get()); // threadParam.remove(); }); execService.execute(t3); }); execService.execute(t); TimeUnit.SECONDS.sleep(1); //線程4,線程1同級 Thread t4 = new Thread(()-> { threadParam.set("CBA"); System.out.println("t4:" + threadParam.get()); }); execService.execute(t4); } } }
輸出結果:
t1:abc
t2:abc
t3:abc
t4:CBA
t1:abc
t2:abc
t3:abc
t4:CBA
t1:abc
t2:abc
t3:CBA //因復用線程而導致問題
t4:CBA
Runnable只是線程方法,Thread才是線程,需要給Runnable加上一個線程的殼,調用start才會使用線程執行.
這里線程池只存活3個線程,那么在線程池復用線程(殼)的時候,殼的屬性只有在創建的時候才會被重新設置值(如果有操作的話,例如:InheritableThreadLocal,ThreadLocal).
這些殼被創建好以后提交給了線程池,但是線程方法並沒有馬上執行,然后被其他殼修改了屬性.當這個線程方法開始執行的時候,已經不是自己創建的殼了
這里線程3,因為由於線程切換使用了被線程4修改以后的殼的屬性.
加大線程池,以滿足每個線程方法獨立使用一個線程只能保證第一次運行正確,因為沒有涉及Thread重用的問題.但是如果涉及重用Thread(殼)的時候,沒有辦法可以保證.
本篇問題還未結束...使用線程池如何Remove ThreadLocal參數...使用線程池其他提交方法結果也不一樣等等,有空再補充.