面試官:ThreadLocal的應用場景和注意事項有哪些?

前言

ThreadLocal主要有如下2個作用

  1. 保證線程安全
  2. 在線程級別傳遞變量

保證線程安全

最近一個小夥伴把項目中封裝的日期工具類用在多線程環境下居然出了問題,來看看怎麼回事吧

日期轉換的一個工具類

<code>public class DateUtil {

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

public static Date parse(String dateStr) {
Date date = null;
try {
date = sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}/<code>

然後將這個工具類用在多線程環境下

<code>public static void main(String[] args) {

ExecutorService service = Executors.newFixedThreadPool(20);

for (int i = 0; i < 20; i++) {
service.execute(()->{
System.out.println(DateUtil.parse("2019-06-01 16:34:30"));
});
}
service.shutdown();
}/<code>

結果報異常了,因為部分線程獲取的時間不對

面試官:ThreadLocal的應用場景和注意事項有哪些?

這個異常就不從源碼的角度分析了,寫一個小Demo,理解了這個小Demo,就理解了原因

一個將數字加10的工具類

<code>public class NumUtil {

public static int addNum = 0;

public static int add10(int num) {
addNum = num;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return addNum + 10;
}
}/<code>
<code>public static void main(String[] args) {

\tExecutorService service = Executors.newFixedThreadPool(20);

\tfor (int i = 0; i < 20; i++) {
\t\tint num = i;
\t\tservice.execute(()->{
\t\t\tSystem.out.println(num + " " + NumUtil.add10(num));
\t\t});
\t}
\tservice.shutdown();
}/<code>

然後代碼的一部分輸出為

什麼鬼,不是加10麼,怎麼都輸出了28?這主要是因為線程切換的原因,線程陸續將addNum值設置為0 ,3,7但是都沒有執行完(沒有執行到return addNum+10這一步)就被切換了,當其中一個線程將addNum值設置為18時,線程陸續開始執行addNum+10這一步,結果都輸出了28。SimpleDateFormat的原因和這個類似,那麼我們如何解決這個問題呢?

解決方案

解決方案1:每次來都new新的,空間浪費比較大

<code>public class DateUtil {

public static Date parse(String dateStr) {
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = null;
try {
date = sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}/<code>

解決方案2:方法用synchronized修飾,併發上不來

<code>public class DateUtil {

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

public static synchronized Date parse(String dateStr) {
Date date = null;
try {
date = sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}/<code>

解決方案3:用jdk1.8中的日期格式類DateFormatter,DateTimeFormatter

<code>public class DateUtil {

private static DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

public static LocalDateTime parse(String dateStr) {
return LocalDateTime.parse(dateStr, formatter);
}
}/<code>

解決方案4:用ThreadLocal,一個線程一個SimpleDateFormat對象

<code>public class DateUtil {

private static ThreadLocal<dateformat> threadLocal = ThreadLocal.withInitial(
()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public static Date parse(String dateStr) {
Date date = null;
try {
date = threadLocal.get().parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}/<dateformat>/<code>

上面的加10的工具類可以改成如下形式(主要為了演示ThreadLocal的使用)

<code>public class NumUtil {

private static ThreadLocal<integer> addNumThreadLocal = new ThreadLocal<>();

public static int add10(int num) {
addNumThreadLocal.set(num);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return addNumThreadLocal.get() + 10;
}
}/<integer>/<code>

現在2個工具類都能正常使用了,這是為啥呢?

原理分析

當多個線程同時讀寫同一共享變量時存在併發問題,如果不共享不就沒有併發問題了,一個線程存一個自己的變量,類比原來好幾個人玩同一個球,現在一個人一個球,就沒有問題了,如何把變量存在線程上呢?其實Thread類內部已經有一個Map容器用來存變量了。它的大概結構如下所示

面試官:ThreadLocal的應用場景和注意事項有哪些?

ThreadLocalMap是一個Map,key是ThreadLocal,value是Object

映射到源碼就是如下所示:

ThreadLocalMap是ThreadLocal的一個靜態內部類

<code>public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}/<code>

往ThreadLocalMap裡面放值

<code>// ThreadLocal類裡面的方法,將源碼整合了一下
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if (map != null)
map.set(this, value);
else
\t\tt.threadLocals = new ThreadLocalMap(this, firstValue);
}/<code>

從ThreadLocalMap裡面取值

<code>// ThreadLocal類裡面的方法,將源碼整合了一下
public T get() {
\tThread t = Thread.currentThread();
\tThreadLocalMap map = t.threadLocals;
\tif (map != null) {
\t\tThreadLocalMap.Entry e = map.getEntry(this);
\t\tif (e != null) {
\t\t\t@SuppressWarnings("unchecked")
\t\t\tT result = (T)e.value;
\t\t\treturn result;
\t\t}
\t}
\treturn setInitialValue();
}/<code>

從ThreadLocalMap裡面刪除值

<code>// ThreadLocal類裡面的方法,將源碼整合了一下
public void remove() {
\tThreadLocalMap m = Thread.currentThread().threadLocals;
\tif (m != null)
\t\tm.remove(this);
}/<code>

執行如下代碼

<code>public class InfoUtil {

private static ThreadLocal<string> nameInfo = new ThreadLocal<>();
private static ThreadLocal<integer> ageInfo = new ThreadLocal<>();

public static void setInfo(String name, Integer age) {
nameInfo.set(name);
ageInfo.set(age);
}

public static String getName() {
return nameInfo.get();
}

public static void main(String[] args) {
new Thread(() -> {
InfoUtil.setInfo("張三", 10);
// 張三
System.out.println(InfoUtil.getName());
}, "thread1").start();
new Thread(() -> {
InfoUtil.setInfo("李四", 20);
// 李四
System.out.println(InfoUtil.getName());
}, "thread2").start();
}
}/<integer>/<string>/<code>

變量的結構如下圖

面試官:ThreadLocal的應用場景和注意事項有哪些?

在線程級別傳遞變量

假設有如下一個場景,method1()調用method2(),method2()調用method3(),method3()調用method4(),method1()生成了一個變量想在method4()中使用,有如下2種解決辦法

  1. method 2 3 4的參數列表上都寫上method4想要的變量
  2. method 1 往ThreadLocal中put一個值,method4從ThreadLocal中get出來

哪種實現方式比較優雅呢?相信我不說你也能明白了

我在生產環境中一般是這樣用的,如果一個請求在系統中的處理流程比較長,可以對請求的日誌打一個相同的前綴,這樣比較方便處理問題

這個前綴的生成和移除可以配置在攔截器中,切面中,當然也可以在一個方法的前後

<code>public class Main {

public static final ThreadLocal<string> SPANID =
ThreadLocal.withInitial(() -> UUID.randomUUID().toString());

public static void start() {
SPANID.set(UUID.randomUUID().toString());
// 方法調用過程中可以在日誌中打印SPANID表明一個請求的執行鏈路
SPANID.remove();
}

}/<string>/<code>

當然Spring Cloud已經有現成的鏈路追蹤組件了。

ThreadLocal使用注意事項

ThreadLocal如果使用不當會造成如下問題

  1. 髒數據
  2. 內存洩露

髒數據

線程複用會造成髒數據。由於線程池會複用Thread對象,因此Thread類的成員變量threadLocals也會被複用。如果在線程的run()方法中不顯示調用remove()清理與線程相關的ThreadLocal信息,並且下一個線程不調用set()設置初始值,就可能get()到上個線程設置的值

內存洩露

<code>static class ThreadLocalMap {

\tstatic class Entry extends WeakReference<threadlocal>> {
\t\tObject value;

\t\tEntry(ThreadLocal> k, Object v) {
\t\t\tsuper(k);
\t\t\tvalue = v;
\t\t}
\t}
}/<threadlocal>/<code>

ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈: Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永遠無法回收,造成內存洩漏

大白話一點,ThreadLocalMap的key是弱引用,GC時會被回收掉,那麼就有可能存在ThreadLocalMap<null>的情況,這個Object就是洩露的對象/<null>

其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap裡所有key為null的value


解決辦法

解決以上兩個問題的辦法很簡單,就是在每次用完ThreadLocal後,及時調用remove()方法清理即可


分享到:


相關文章: