ThreadLocal
定義
- 網上很多說
ThreadLocal是處理并發多執行緒的,根據官方定義:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
For example, the class below generates unique identifiers local to each thread. A thread’s id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls
- 簡單的說
ThreadLocal是個執行緒區域變數,每個執行緒都保持有這個變數的副本,在執行緒的各個方法呼叫之間傳遞資料,重要的事情說三遍傳遞資料、傳遞資料、傳遞資料,下面就從幾個常用的方式來說明ThreadLocal在日常編程中的使用和常見的哪些坑
基本使用
- 執行緒方法之間傳遞資料,下面例子中的兩個執行緒中的每個方法中獲取的
ThreadLocal的值都是一樣的

Thread-0 m1 threadLocal value = 1
Thread-1 m1 threadLocal value = 1
Thread-0 m2 threadLocal value = 1
Thread-1 m2 threadLocal value = 1
- 兩個執行緒中執行m1和m2,
ThreadLocal的值都是一樣的,說明確實可以在方法中間傳遞資料
坑一、格式化時間問題
- 通過
SimpleDateFormat在并發訪問格式化時間的時候會出現時間錯亂,因為SimpleDateFormat方法不是執行緒安全的,解決方法可以通過ThreadLocal保存副本,每個執行緒之間變數隔離,也可以采用java8的DateTimeFormatter來格式化時間 - 下面這段代碼執行兩次,一次是執行
SimpleDateFormat來格式化時間,另外一次是通過把SimpleDateFormat放在ThreadLocal來執行結果如何??
public class ThreadLocalFormatDemo {
private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final static ThreadLocal<SimpleDateFormat> THREAD_LOCAL =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
// 多執行緒時間格式化問題
/*new Thread(()->{
try {
Date date = new Date();
String dateStr1 = ThreadLocalFormatDemo.format.format(date);
Date date1 = format.parse(dateStr1);
String dateStr2 = ThreadLocalFormatDemo.format.format(date1);
System.out.println(dateStr1.equals(dateStr2));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();*/
// 多執行緒ThreadLocal 避免時間格式化的問題
new Thread(()->{
try {
Date date = new Date();
String dateStr1 = ThreadLocalFormatDemo.THREAD_LOCAL.get().format(date);
Date date1 = THREAD_LOCAL.get().parse(dateStr1);
String dateStr2 = ThreadLocalFormatDemo.THREAD_LOCAL.get().format(date1);
System.out.println(dateStr1.equals(dateStr2));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}

- 圖片左邊是
SimpleDateFormat結果,出現false說明時間出現不一致,右邊是ThreadLocal,回傳的都是true,結果符合預期,可以看出右邊的明顯是執行緒安全的 - java8 中的代碼注解,看得出來類是執行緒安全的,建議使用
/**...
* @implSpec
* This class is immutable and thread-safe.
*
* @since 1.8
*/
public final class DateTimeFormatter {
坑二、記憶體泄漏
- Thread中TheadLocal的定義為:
ThreadLocal.ThreadLocalMap threadLocals = null;,說明在Thread中初始化后真正保存的是ThreadLocal.ThreadLocalMap,也就是一個Map,該Map中的key指向的是代碼中自己定義的ThreadLocal靜態變數,關鍵是這個參考是一個弱參考,而弱參考在發生GC的時候會被清理掉(畫外音:強參考,軟參考,弱參考…可以自行查閱),那么如果key的參考在GC被清理掉的話而此時呼叫的執行緒沒有停止或者是執行緒池,會出現執行緒參考了該Map而Map中的key的參考已經被GC掉,根據GC Root原則由于Map被Thread參考不會被清理掉,也就是說,如果Thread一直存在,而且ThreadLocalMap中可以set值,最終可能導致記憶體泄漏(畫外音:個人見解),直接上代碼 - 執行代碼如下:
public class ThreadlocalLeakDemo {
private static ThreadLocal<List<String>> listThreadLocal =
ThreadLocal.withInitial(ArrayList::new);
/**
* 一個執行緒不停胡添加資料,后臺打開jvisualvm執行gc,查看記憶體是否有記憶體泄漏風險
* @param args
*/
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
for (int j = 0; j < Integer.MAX_VALUE; j++) {
try {
listThreadLocal.get().add("我是ThreadLocal變數,查看在GC之后是否存在記憶體泄漏的風險" + j);
TimeUnit.MILLISECONDS.sleep(3L);
if (j == 20000) {
System.out.printf("j = %d,執行60秒之后退出回圈\n", j);
// 第一次手動執行gc并執行dump操作
TimeUnit.SECONDS.sleep(60L);
System.out.println("退出回圈");
// 列印上述日志之后第二次執行gc并執行dump操作
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"threadlocal").start();
System.out.println("main 等待中...");
TimeUnit.SECONDS.sleep(Long.MAX_VALUE);
}
}
- 根據上述的代碼執行之后截取兩次的dump如下圖,可以明顯的看出來在執行緒退出之前即使執行GC還是有物件(讀者可以在自己的機器執行,在沒有執行上述代碼的
System.out.printf("j = %d,執行60秒之后退出回圈\n", j);之前)增加,而在Thread退出之后執行GC之后則堆記憶體顯著下降,可以看出在Thread沒有退出的情況下,確實存在有記憶體泄漏的風險
- 解決方案:需要在執行緒執行退出之前在finally執行ThreadLocal的remove方法,洗掉map中的資料
坑三、資料背景關系錯亂
- 如果是執行緒池來執行任務,對
ThreadLocal的操作會造成資料錯亂
public class ThreadLocalDataMix {
private static ThreadLocal<AtomicInteger> threadLocal =
ThreadLocal.withInitial(() -> new AtomicInteger(0));
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 4; i++) {
executorService.submit(()->{
try {
threadLocal.get().getAndIncrement();
System.out.printf("當前執行緒%s, threadLocal value = %d\n", Thread.currentThread().getName(), threadLocal.get().get());
} finally {
// 執行該代碼洗掉ThreadLocalMap中的資料,不執行則跟執行緒系結一直存在
threadLocal.remove();
}
});
}
executorService.shutdown();
}
}
- 結果1:注釋
threadLocal.remove();改代碼
當前執行緒pool-1-thread-1, threadLocal value = 1
當前執行緒pool-1-thread-2, threadLocal value = 1
當前執行緒pool-1-thread-2, threadLocal value = 2
當前執行緒pool-1-thread-1, threadLocal value = 2
- 結果2:執行
threadLocal.remove();代碼
當前執行緒pool-1-thread-2, threadLocal value = 1
當前執行緒pool-1-thread-1, threadLocal value = 1
當前執行緒pool-1-thread-1, threadLocal value = 1
當前執行緒pool-1-thread-1, threadLocal value = 1
- 可以看出結果2是正常執行回傳結果,結果1原因是在執行緒池中的Map物件沒有被清空造成了下次繼續執行造成的資料錯亂
總結
ThreadLocal是Thread的好搭檔,好多框架都用它在方法之間來傳遞變數,但是一定要注意它隱蔽的坑,總而言之可以在程式中的攔截器、過濾器或者其它地方使用finally執行threadLocal.remove方法- 網上有很多關于ThreadLocal的原理的讀者可以自行搜索查閱,我這個也是班門弄斧,有不足還請指正!
你們的點贊和關注是我創作的最大動力,有什么不足和錯誤的地方歡迎留言,微信搜索關注小二說碼,定期分享干貨!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/257055.html
標籤:其他
