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