1. 程式人生 > >Java | 多執行緒 | ThreadLocal結合線程池的正確使用方式

Java | 多執行緒 | ThreadLocal結合線程池的正確使用方式

1)問題:

才發現,寫這篇部落格之前,自己一直在以一種錯誤的姿勢在用threadLocal物件。
場景就是threadLocal在專案中使用時,出現取值錯誤的情況。花了不少時間排查,最終還是排查到執行緒池上。之前一直沒有問題,或許是因為併發不高。最終今天還是遇到了問題(出來混,遲早是要還的)。
不禁開始懷疑:threadLocal遇到執行緒池就不好用了?

2)分析:

我們都知道threadLocal中維護了一個執行緒和value的對映,當前執行緒的threadLocal即為key,value為引用的物件。

t.threadLocals = new ThreadLocalMap(this, firstValue);

每個執行緒儲存一份,達到執行緒安全。
但是線上程複用的情況下,threadLocal並不能保證按照預期執行,很有可能出現數據錯亂。原因就是執行緒池中的執行緒在還未銷燬的情況下,新的請求進來,會繼續複用執行緒池中的執行緒,而這些執行緒在之前處理的過程中,對應的threadLocal有可能已經有值,導致出錯。程式碼如下:

/**
 * Created by zhangshukang on 2018/7/27.
 */
public class ThreadLocalTest {

    public static void main(String[] args) throws InterruptedException {

        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

        ExecutorService pool = new ThreadPoolExecutor(
                1,
                1,
                50000L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(1024),
                new ThreadFactory() {
                    private AtomicInteger number = new AtomicInteger(0);
                    @Override
                    public Thread newThread(Runnable runnable) {
                        return new Thread(runnable, "admin" + "-" + number.getAndIncrement());
                    }
                },
                new ThreadPoolExecutor.AbortPolicy());



        pool.execute(()->{
            System.out.println(Thread.currentThread().getName());
            threadLocal.set(5);
            System.out.println(threadLocal.get());
        });


        Thread.sleep(3000l);
        System.out.println("-------------------");


        pool.execute(()->{
            System.out.println(Thread.currentThread().getName());
            System.out.println(threadLocal.get());
        });

    }
}

執行結果如下:

admin-0
5
-------------------
admin-0
5

這裡只是模擬伺服器執行緒池的執行流程,當web伺服器接收到一個請求處理結束,未清理掉threadLocal中的變數,此時另一個請求進來,同樣用該執行緒去處理。這個時候發現threadLocal中已有變數,如果使用不慎,會出現資料錯誤的情形。

可以看到上面的執行結果:執行緒 admin-0 複用,輸出了相同的變數。

3)正確姿勢:

正確姿勢是在threadLocal變數使用之後,呼叫remove()方法。這麼做也可以避免記憶體洩露,如果沒有呼叫該方法,那當map中當前執行緒被回收,對應的value得不到回收,容易引起記憶體洩露。

這樣又引出了新問題,就是我們怎麼區分在什麼地方進行remove()呢?假如一個介面呼叫了remove(),然後結束了程式碼邏輯,這沒問題。但是有可能另一個介面也執行完該程式碼塊remove(),接著又在別的地方呼叫了get()方法,這種場景還是會出錯。
所以儘量避免這種使用場景,要保證remove()的程式碼塊的呼叫鏈後面,不會再執行get方法。

友鏈:探果網