使用ThreadLocal差點讓我懷疑自己見鬼了
前言
最近使用ThreadLocal出現了一個生產問題
一大清早就接到業務人員的電話,說系統登錄進去后總是莫名其妙的報錯,而且有點隨機...昏沉的腦袋瞬間清醒了,我問具體是哪個模塊報錯,是不是操作了哪些特定的功能才報錯,得到的回答是否定的,任何功能操作都隨機報錯??,也就是有時候報錯,有時候不報錯。
一時間有點懵逼了,腦海里不斷回憶這段時間是不是上了什么新版本,不對啊,最近也沒有什么大版本啊,都是一些小改,不可能會影響到所有業務模塊啊。
趕忙起床去公司~
到公司后趕忙去機房,查看后臺日志,發現報的是空指針異常,接著繼續定位代碼,發現是這段代碼是從鏈路日志模塊報出來的,仔細看了下代碼,發現報錯是從鏈路日志那塊報出來的,這塊代碼看起來也沒啥問題,而且這個模塊都投產好幾個月了,從來都沒有發生過類似的報錯,跟了下代碼,是從ThreadLocal中取值,第一反應是鏈路日志又問題,先不管了,業務催的緊,先把應用重啟了。
說來也奇怪,重啟后應用竟然沒有再出現報錯了,真的絕了,這下我更加好奇了,在開發環境進行debug,那塊代碼邏輯的偽代碼如下
- // 偽代碼
- 1、ThreadLocal的初始化
- 2、ThreadLocal threadlocal = new ThreadLocal();
- 3、if(threadlocal.get() == null) threadlocal.set(XX)
- 4、....相關業務代碼
- 5、threadlocal.get() 獲取鏈路日志相關信息進行相關的處理
- 6、threadlocal.remove()
咋一看,沒啥問題,然而由于異常的信息導致第4步出現了異常,catch住了但是沒有在finally里操作threadlocal.remove(),又因為第3步的判空對該線程無效了(這個線程已經被設置值了),從而該線程被污染了,
也就是每次用到這個被污染的線程就會報錯,生產的隨機報錯就是這么來的,話不多說修bug。至此問題也解決了。
吸取教訓:使用ThreadLocal時一定要記得考慮清楚場景,把各種情況都考慮全。
下面是對ThreadLocal的一些操作
沒有進行remove操作
- static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
- // 沒有進行remove操作的ThreadLocal的表現
- public static void main(String[] args) throws InterruptedException {
- // 創建一個線程池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- for (int i = 0; i <= 5; i++) {
- final int count = i;
- pool.execute(()->{
- Integer integer = threadLocal.get();
- System.out.println("******************線程" + Thread.currentThread().getName().substring(7) + "設置threadlocal前的值是: " + integer);
- if (StringUtils.isEmpty(threadLocal.get())) {
- threadLocal.set(count);
- }
- System.out.println("******************線程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get());
- });
- Thread.sleep(100);
- }
- }
控制臺打印效果如下,得到錯誤答案
進行了remove操作
- static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
- // 進行remove操作的ThreadLocal的表現
- public static void main(String[] args) throws InterruptedException {
- // 創建一個線程池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- for (int i = 0; i <= 5; i++) {
- final int count = i;
- pool.execute(()->{
- Integer integer = threadLocal.get();
- System.out.println("******************線程" + Thread.currentThread().getName().substring(7) + "設置threadlocal前的值是: " + integer);
- if (StringUtils.isEmpty(threadLocal.get())) {
- threadLocal.set(count);
- }
- System.out.println("******************線程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get());
- threadLocal.remove();
- });
- Thread.sleep(100);
- }
- }
控制臺打印效果如下,得到正確答案
remove操作報錯了
- static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
- // 沒有進行remove操作的ThreadLocal的表現
- public static void main(String[] args) throws InterruptedException {
- // 創建一個線程池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- for (int i = 0; i <= 5; i++) {
- final int count = i;
- pool.execute(()-> {
- try {
- Integer integer = threadLocal.get();
- System.out.println("******************線程" + Thread.currentThread().getName().substring(7) + "設置threadlocal前的值是: " + integer);
- if (StringUtils.isEmpty(threadLocal.get())) {
- threadLocal.set(count);
- }
- if (Thread.currentThread().getName().contains("thread-1")) {
- throw new RuntimeException();
- }
- System.out.println("******************線程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get());
- threadLocal.remove();
- } catch (Exception e) {}
- });
- Thread.sleep(100);
- }
- }
控制臺打印效果如下,雖然進行了catch但是沒有在finally里進行remove操作,得到錯誤答案
再修改得到最終代碼
- static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
- // 沒有進行remove操作的ThreadLocal的表現
- public static void main(String[] args) throws InterruptedException {
- // 創建一個線程池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- for (int i = 0; i <= 5; i++) {
- final int count = i;
- pool.execute(()-> {
- try {
- Integer integer = threadLocal.get();
- System.out.println("******************線程" + Thread.currentThread().getName().substring(7) + "設置threadlocal前的值是: " + integer);
- if (StringUtils.isEmpty(threadLocal.get())) {
- threadLocal.set(count);
- }
- if (Thread.currentThread().getName().contains("thread-1")) {
- throw new RuntimeException();
- }
- System.out.println("******************線程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get());
- } catch (Exception e) {.....}
- finally {
- threadLocal.remove();
- }
- });
- Thread.sleep(100);
- }
- }
ThreadLocal用于線程間的數據隔離,一說到線程間的數據隔離,我們還能想到synchronized或者其他的鎖來實現線程間的安全問題。
ThreadLocal適合什么樣的業務場景
1、使用threadlocal存儲數據庫連接,如果說一次線程請求,需要同時更新Goods表和Goods_Detail表,要是直接new出2個數據庫連接,那么事務就沒法進行保障了,數據庫連接池
使用ThreadLocal來存儲數據庫連接對象Connection,從而每次操作數據庫表都是使用同一個對象保障了事務。
2、解決SimpleDataFormat的線程安全問題
3、基于hreadlocal的數據源的動態切換
4、使用ThreadLocal來存儲Cookie對象,在這次Http請求中,任何時候都可以通過簡單的方式獲取到Cookie。
當ThreadLocal被設置后綁定了當前線程,如果線程希望當前線程的子線程也能獲取到該值,這就是InheritableThreadLocal的用武之地了
如何傳遞給子線程呢?InheritableThreadLocal的具體使用如下:
- // 創建InheritableThreadLocal
- static ThreadLocal<Integer> threadLocaltest = new InheritableThreadLocal<>();
- public static void main(String[] args) {
- // 主線程設置值
- threadLocaltest.set(100);
- new Thread(()-> {
- // 子線程獲取值
- Integer num = threadLocaltest.get();
- // 子線程獲取到值并打印出來
- System.out.println(Thread.currentThread().getName() + "子類獲取到的值" + num); // 輸出:Thread-0子類獲取到的值100
- }).start();
- }