ThreadLocal基礎部分

ThreadLoal的作用

儲存執行緒的獨立變數,即每個執行緒維護一份。這種變數線上程的生命週期內起作用,減少同一個執行緒內多個函式之間公共變數傳遞麻煩。

使用場景

需要給不同的執行緒儲存不同的資訊時。

基礎使用

public class TestThreadLocal {

    private static ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>();
// private static ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){
//
// @Override
// protected Integer initialValue() {
// return 0;
// }
// }; public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadLocal.get());
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set(3);
System.out.println("t2:"+threadLocal.get());
}
}); t1.start();
t2.start();
System.out.println(threadLocal.get()); }
}

如果需要設定預設值的話,可以實現initialValue方法。

典型場景1:我們知道SimpleDateFormat的物件如果多執行緒使用的話會有執行緒不安全的問題。具體程式碼如下:

public class TestThreadLocal {

    public static ExecutorService executorService = Executors.newFixedThreadPool(16);

    private static SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

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

       for (int i=0;i<1000;i++){
executorService.submit(new Runnable() {
@Override
public void run() {
String format = simpleDateFormat.format(new Date());
try {
Date parse = simpleDateFormat.parse("2021-09-01 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(format);
}
}); }
Thread.sleep(3000);
executorService.shutdownNow(); }
}

執行結果如下:

可以看出,發生了異常。

方法1:我們可以改為每次都new一個新的SimpleDateFormat物件的話,這樣再執行是沒問題的。但是有些資源浪費。

方法2:使用ThreadLocal來解決。假設執行緒池裡共16個執行緒,那我們總共16個SimpleDateFormat物件就可以應付所有的日期格式化的呼叫。

程式碼如下:

public class TestThreadLocal {

    public static ExecutorService executorService = Executors.newFixedThreadPool(16);

    private static ThreadLocal<SimpleDateFormat> threadLocal=new ThreadLocal<SimpleDateFormat>(){

        @Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
}; private static SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) throws InterruptedException { for (int i=0;i<1000;i++){
executorService.submit(new Runnable() {
@Override
public void run() {
String format = threadLocal.get().format(new Date());
try {
Date parse = threadLocal.get().parse("2021-09-01 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(format);
}
}); }
Thread.sleep(3000);
executorService.shutdownNow(); }
}

注意: 如果不使用執行緒池,執行緒結束,執行緒裡的threadLocalMap也會被回收。但是如果使用執行緒池,執行緒池裡面的執行緒會被複用,執行緒裡的threadLocalMap不會被回收,就造成了記憶體洩漏。按照正確的使用方法應該是每次用完了remove,但是這樣效率就很低。還不如方法1每次去new一個新的SimpleDateFormat物件。(但個人覺得其實還好,洩漏一點也沒關係,不過threadlocal畢竟不是專門解決執行緒安全問題的,不推薦這麼用)

正確使用方法

  • 每次使用完ThreadLocal都呼叫它的remove()方法清除資料
  • 將ThreadLocal變數定義成private static,這樣就一直存在ThreadLocal的強引用,也就能保證任何時候都能通過ThreadLocal的弱引用訪問到Entry的value值,進而清除掉 。

ThreadLocal 高階部分

ThreadLocal為什麼會記憶體洩露?

記憶體洩漏(Memory Leak)是指程式中已動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰等嚴重後果。

試想:

一個執行緒對應一塊工作記憶體,執行緒可以儲存多個ThreadLocal。那麼假設,開啟1萬個執行緒,每個執行緒建立1萬個ThreadLocal,也就是每個執行緒維護1萬個ThreadLocal小記憶體空間,而且當執行緒執行結束以後,假設這些ThreadLocal裡的Entry還不會被回收,那麼將很容易導致堆記憶體溢位。

怎麼辦?難道JVM就沒有提供什麼解決方案嗎?

答案:

  1. JVM利用設定ThreadLocalMap的Key為弱引用,來避免記憶體洩露。
  2. JVM利用呼叫remove、get、set方法的時候,順道回收髒value值。

ThreadLocal的關係圖如下所示:

Thread裡面維護了一個ThreadLocalMap,這個map裡面的key是弱引用的readLocal例項。value是我們設定進去的值。當把treadLocal例項物件置為null後,沒有任何強引用指向threadLocal例項,所以theadLocal將會被gc回收。但是我們的value不會被回收,因為存在一個thread連線過來的強引用。只有當thread結束後,強引用斷開,map、value等將全部被回收。

如下圖:

但是很多時候我們使用執行緒池,為了複用執行緒,thread生命週期沒有結束,所以無法回收,造成記憶體洩漏。