一、常見場景
1、ThreadLocal作為執行緒背景關系副本,那么一種最常見的使用方式就是用來方法隱式傳參,通過提供的set()和get()兩個public方法來實作在不同的方法中的引數傳遞,對于編程規范來說,方法定義的時候是對引數個數是有限制的,甚至在一些大廠,對方法引數個數是有明確規定的,
2、執行緒安全,每個執行緒維持自己的變數,以免紊亂,像常用的資料庫的連接池的執行緒安全實作就使用了ThreadLocal,
二、進階使用
以引數傳遞為例子,如何更好地使用ThreadLocal來實作在同一執行緒堆疊中不同方法中的引數傳遞,在引數傳遞的時候,那么都會有引數名,引數值,而ThreadLocal提供的get()和set()方法,不能直接滿足設定引數名和引數值,這種情況下就需要對ThreadLocal進一次封裝,如下代碼,維護一個map物件,然后提供setValue(key, value)和getValue(key, value)方法,就可以很方便地實作了引數的設定和獲取;在需要的地方對引數進行清理,使用remove(key)或者clear()即可實作,
import java.util.HashMap;
import java.util.Map;
public class ThreadLocalManger<T> extends ThreadLocal<T> {
private static ThreadLocalManger<Map<String, Object>> MANGER = new ThreadLocalManger<>();
private static HashMap<String, Object> MANGER_MAP = new HashMap<>();
public static void setValue(String key, Object value) {
Map<String, Object> context = MANGER.get();
if(context == null) {
synchronized (MANGER_MAP) {
if(context == null) {
context = new HashMap<>();
MANGER.set(context);
}
}
}
context.put(key, value);
}
public static Object getValue(String key) {
Map<String, Object> context = MANGER.get();
if(context != null) {
return context.get(key);
}
return null;
}
public static void remove(String key) {
Map<String, Object> context = MANGER.get();
if(context != null) {
context.remove(key);
}
}
public static void clear() {
Map<String, Object> context = MANGER.get();
if(context != null) {
context.clear();
}
}
}
三、使用漏洞
繼續以引數傳遞為例子,來看看ThreadLocal使用程序中存在的問題和后果,在實際業務的功能開發中,為了提升效率,大部分情況下都會使用執行緒池來實作,比如資料庫的連接池、RPC請求連接池、MQ訊息處理池、后臺批量job池等等;同時也可能會使用一個伴隨整個應用生命周期的執行緒(守護執行緒)來實作的一些功能,比如說心跳、監控等等,使用執行緒池,那么在實際生產業務中并發肯定不低,池中執行緒就會一直復用;守護執行緒一旦創建,那么就會活到應用停機,所以在這些情況下,執行緒的生命周期很長,在使用ThreadLocal的時候,一定要進行清理,不然就會有記憶體溢位的情況發生,通過以下案例來模擬記憶體溢位的情況,
通過一個死回圈來模擬高并發場景,創建一個10個核心執行緒數,10個最大執行緒數數,60秒空閑時間的、執行緒名以ThreadLocal-demo-開頭的執行緒池,在該場景下,將有10個執行緒來運行,運行內容很簡單:生成一個UUID,并將其作為引數key,然后設定到執行緒副本中,
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;
import java.util.UUID;
import java.util.concurrent.*;
@Service
public class ThreadLocalService {
ThreadFactory springThreadFactory = new CustomizableThreadFactory("TheadLocal-demo-");
ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(), springThreadFactory);
ExecutorService service = new ThreadPoolExecutor(10, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>());
public Object setValue() {
for(; ;) {
try {
Runnable runnable = new Runnable() {
@Override
public void run() {
String id = UUID.randomUUID().toString();
// add
ThreadLocalManger.setValue(id, "this is a value");
//do something here
ThreadLocalManger.getValue(id);
// clear()
//ThreadLocalManger.clear();
}
};
executorService.submit(runnable);
} catch (Exception e) {
e.printStackTrace();
break;
}
}
return "success";
}
}
以上代碼中已把clear()方法注釋掉,不做清理,觸發程式,稍微將jvm設定低一些,跑不久就會報如下OOM,
java.lang.OutOfMemoryError: GC overhead limit exceeded
Exception in thread "TheadLocal-demo-9"
Exception in thread "TheadLocal-demo-8"
Exception in thread "TheadLocal-demo-6"
Exception in thread "TheadLocal-demo-10"
Exception in thread "TheadLocal-demo-7"
java.lang.OutOfMemoryError: GC overhead limit exceeded
Exception in thread "TheadLocal-demo-5"
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
at com.intellij.rt.debugger.agent.CaptureStorage.insertEnter(CaptureStorage.java:57)
at java.util.concurrent.FutureTask.run(FutureTask.java)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
就會發生嚴重的記憶體溢位,通過如下debug截圖可知,設定進去的UUID堆積在記憶體中,逐步變多,最終撐爆記憶體,
在實際的業務場景中,需要傳遞的可能有訂單號,交易號,流水號等等,這些變數往往是唯一不重復的、符合案例中的UUID情況,在不清理的情況下就會造成應用OOM,進而不可用;在分布式系統中,還能導致上下游系統不可用,進而導致整個分布式進去的不可用;如果這些資訊往往還可能用在網路傳輸中,大訊息占有網路帶寬,嚴重甚至導致網路癱瘓,所以一個小小的細節就會置整個集群于危險之中,那么如何合理化解呢,
四、終階使用
以上問題在于忘記清理,那么如何讓清理無感知,即不需要清理也沒有問題,根因在于執行緒跑完一次之后,沒有進行清理,所以可提供一個基類執行緒,在執行緒執行最后對清理進行封裝,如下代碼,提供一個BaseRunnable抽象基類,該類主要如下特點,
1、該類繼承Runnable,
2、實作setArg(key, value)和getArg(key)兩個方法,
2、在重寫的run方式中分為兩步,第一步,呼叫抽象方法task;第二步,清理執行緒副本,
有了以上3個特點,繼承了BaseRunnable的執行緒類,只需要在實作task方法,在task方法中實作業務邏輯,引數傳遞和獲取通過setArg(key, value)和getArg(key)兩個方法即可實作,無需再顯示清理,
public abstract class BaseRunnable implements Runnable {
@Override
public void run() {
try {
task();
} finally {
ThreadLocalManger.clear();
}
}
public void setArg(String key, String value) {
ThreadLocalManger.setValue(key, value);
}
public Object getArg(String key) {
return ThreadLocalManger.getValue(key);
}
public abstract void task();
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/413308.html
標籤:其他
上一篇:我的 2021 年終總結
