Java項(xiàng)目開發(fā)中ThreadLocal的6大用法總結(jié)
前言
你的系統(tǒng)需要記錄每個(gè)用戶的操作日志,包括用戶ID、操作時(shí)間、操作內(nèi)容等。在單線程環(huán)境下,這很簡單,一個(gè)全局變量就夠了。
但到了Web應(yīng)用中,一個(gè)請求一個(gè)線程,多個(gè)用戶同時(shí)操作,怎么保證A用戶的操作不會被記錄成B用戶呢?
你可能會想到在每個(gè)方法里都傳遞用戶信息,但這樣太麻煩了。
這就是 ThreadLocal 要解決的核心問題:在多線程環(huán)境下,如何在不傳遞參數(shù)的情況下,讓同一個(gè)線程內(nèi)的多個(gè)方法共享數(shù)據(jù),同時(shí)又不會影響其他線程?
ThreadLocal是什么
簡單理解,ThreadLocal 是一個(gè)讓每個(gè)線程擁有自己獨(dú)立變量副本的容器。
對同一個(gè) ThreadLocal 實(shí)例的 get()/set(),不同線程看到的是不同的數(shù)據(jù),互不干擾。
看個(gè)最簡單的例子:
public class UserContext {
// 創(chuàng)建一個(gè)ThreadLocal變量,用來存儲用戶ID
private static final ThreadLocal<String> userIdHolder = new ThreadLocal<>();
// 設(shè)置用戶ID
public static void setUserId(String userId) {
userIdHolder.set(userId);
}
// 獲取用戶ID
public static String getUserId() {
return userIdHolder.get();
}
// 清理用戶ID
public static void clear() {
userIdHolder.remove();
}
}
使用起來是這樣的:
try {
UserContext.setUserId(userId);
// 業(yè)務(wù)處理...
} finally {
// 確保處理完請求后,無論如何都會清理
UserContext.clear();
}
// 在后續(xù)的業(yè)務(wù)邏輯中,任何地方都可以獲取到這個(gè)用戶ID
String userId = UserContext.getUserId();
是不是很方便?不用在每個(gè)方法參數(shù)里傳遞用戶ID,任何地方都可以獲取到當(dāng)前線程的用戶信息。
ThreadLocal是怎么工作的
很多人以為 ThreadLocal 只是簡單的把變量復(fù)制到每個(gè)線程,其實(shí)不是這樣。
它的原理是這樣的:
- 每個(gè)線程(Thread對象)內(nèi)部都有一個(gè)特殊的Map,叫做
ThreadLocalMap。 - 當(dāng)你調(diào)用
threadLocal.set(value)時(shí),實(shí)際上是把 value 放到了當(dāng)前線程的ThreadLocalMap中,key 是這個(gè) ThreadLocal 對象。 - 當(dāng)你調(diào)用
threadLocal.get()時(shí),實(shí)際上是去當(dāng)前線程的 ThreadLocalMap 中,找這個(gè) ThreadLocal 對象對應(yīng)的 value。
可以想象成每個(gè)線程都有一個(gè)小本本(ThreadLocalMap),這個(gè)小本本上記錄著各種 ThreadLocal 變量的值。
當(dāng)你調(diào)用某個(gè) ThreadLocal 的 get 方法,就是在小本本上查找對應(yīng)的內(nèi)容。
ThreadLocal 使用場景和示例
1. 用戶會話管理(最常用)
在Web開發(fā)中,這是最經(jīng)典的使用場景。用戶登錄后,將用戶信息存入 ThreadLocal,在后續(xù)的任何地方都能獲取。
// 用戶上下文工具類
public class UserContext {
private static final ThreadLocal<UserInfo> currentUser = new ThreadLocal<>();
public static void setUser(UserInfo user) {
currentUser.set(user);
}
public static UserInfo getUser() {
return currentUser.get();
}
public static Long getUserId() {
UserInfo user = currentUser.get();
return user != null ? user.getId() : null;
}
public static String getUsername() {
UserInfo user = currentUser.get();
return user != null ? user.getUsername() : null;
}
public static void clear() {
currentUser.remove();
}
}
// 在攔截器中設(shè)置用戶信息
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 從token或session中獲取用戶信息
String token = request.getHeader("Authorization");
UserInfo user = authService.getUserByToken(token);
if (user != null) {
UserContext.setUser(user);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 請求結(jié)束后清理ThreadLocal,防止內(nèi)存泄漏
UserContext.clear();
}
}
// 在業(yè)務(wù)代碼中直接使用
@Service
public class OrderService {
public void createOrder(Order order) {
// 直接獲取當(dāng)前用戶ID,不需要從參數(shù)傳遞
Long userId = UserContext.getUserId();
order.setUserId(userId);
order.setCreateBy(UserContext.getUsername());
// 其他業(yè)務(wù)邏輯...
orderDao.save(order);
// 記錄操作日志
logService.log(userId, "創(chuàng)建訂單");
}
}
2. 日期格式化工具
SimpleDateFormat 是線程不安全的,用 ThreadLocal 可以解決這個(gè)問題。
// 錯誤示例:多線程下會出問題
public class DateUtils {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String format(Date date) {
return sdf.format(date); // 多線程并發(fā)時(shí)會出錯!
}
}
// 正確示例:使用ThreadLocal
public class ThreadSafeDateUtils {
// 每個(gè)線程都有自己的SimpleDateFormat實(shí)例
private static final ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) {
return threadLocal.get().format(date);
}
public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}
// 使用后清理(可選,因?yàn)镈ateFormat對象可以復(fù)用)
public static void clear() {
threadLocal.remove();
}
}
3. 分頁信息管理
在分頁查詢中,ThreadLocal可以保存分頁參數(shù),在Service和Dao層都能獲取。
public class PageContext {
private static final ThreadLocal<Integer> PAGE_NO = new ThreadLocal<>();
private static final ThreadLocal<Integer> PAGE_SIZE = new ThreadLocal<>();
public static void setPageInfo(Integer pageNo, Integer pageSize) {
PAGE_NO.set(pageNo);
PAGE_SIZE.set(pageSize);
}
public static Integer getPageNo() {
Integer pageNo = PAGE_NO.get();
return pageNo != null ? pageNo : 1; // 默認(rèn)第一頁
}
public static Integer getPageSize() {
Integer pageSize = PAGE_SIZE.get();
return pageSize != null ? pageSize : 20; // 默認(rèn)20條
}
public static void clear() {
PAGE_NO.remove();
PAGE_SIZE.remove();
}
}
// 在Controller中使用
@GetMapping("/users")
public PageResult<User> getUsers(@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "20") Integer pageSize) {
try {
PageContext.setPageInfo(pageNo, pageSize);
return userService.getUserList();
} finally {
PageContext.clear();
}
}
4. 數(shù)據(jù)庫連接管理
在一些框架中,為了確保同一個(gè)事務(wù)中使用同一個(gè)數(shù)據(jù)庫連接,會用到 ThreadLocal。
public class ConnectionHolder {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static Connection getConnection() {
Connection conn = connectionHolder.get();
if (conn == null) {
conn = DataSourceUtils.getConnection();
connectionHolder.set(conn);
}
return conn;
}
public static void setConnection(Connection conn) {
connectionHolder.set(conn);
}
public static void clearConnection() {
Connection conn = connectionHolder.get();
if (conn != null) {
DataSourceUtils.releaseConnection(conn);
}
connectionHolder.remove();
}
}
5. 全局參數(shù)傳遞
避免在方法調(diào)用鏈中層層傳遞相同參數(shù),簡化代碼。
// 不用ThreadLocal,參數(shù)傳遞很痛苦
public void processOrder(String traceId, String userId, Order order) {
validateOrder(traceId, userId, order);
checkInventory(traceId, userId, order);
createLog(traceId, userId, order);
// ... 更多調(diào)用
}
// 使用ThreadLocal,清爽多了
public void processOrder(Order order) {
// 在入口處設(shè)置
TraceContext.setTraceId(UUID.randomUUID().toString());
UserContext.setUserId(getCurrentUserId());
validateOrder(order);
checkInventory(order);
createLog(order);
}
6. 分布式追蹤ID傳遞
在微服務(wù)架構(gòu)中,一個(gè)請求可能會經(jīng)過多個(gè)服務(wù),需要有一個(gè)traceId來追蹤整個(gè)調(diào)用鏈。
@Component
public class TraceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String traceId = httpRequest.getHeader("X-Trace-ID");
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString();
}
// 設(shè)置到ThreadLocal
TraceContext.setTraceId(traceId);
try {
// 在MDC中也設(shè)置,方便日志打印
MDC.put("traceId", traceId);
// 繼續(xù)處理請求
chain.doFilter(request, response);
} finally {
// 一定要清理?。。?
TraceContext.clear();
MDC.clear();
}
}
}
ThreadLocal 可能出現(xiàn)的問題
1. 內(nèi)存泄漏(最重要的問題?。?/h3>
這是ThreadLocal最容易被忽視,也是最危險(xiǎn)的問題。我們先來看看為什么會內(nèi)存泄漏。
public class MemoryLeakExample {
private static final ThreadLocal<BigObject> holder = new ThreadLocal<>();
public void process() {
// 設(shè)置一個(gè)大對象
holder.set(new BigObject()); // 假設(shè)BigObject占用100MB內(nèi)存
// 執(zhí)行業(yè)務(wù)邏輯...
// ...
// 忘記調(diào)用 holder.remove() 了!
}
}
問題:
- 當(dāng)方法執(zhí)行完后,
ThreadLocal對象本身(holder)可能被回收 - 但
ThreadLocalMap中,BigObject 這個(gè) 100MB 的對象仍然被引用著 - 如果這個(gè)線程是線程池中的線程,會被復(fù)用,永遠(yuǎn)不會被GC回收
- 隨著請求增多,內(nèi)存占用會越來越大,最終導(dǎo)致OOM
為什么會這樣? 看一下 ThreadLocalMap 的內(nèi)部結(jié)構(gòu):
static class ThreadLocalMap {
// Entry繼承了WeakReference,key是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // value是強(qiáng)引用!
}
private Entry[] table;
}
內(nèi)存泄漏的兩種情況:
1.ThreadLocal 對象被回收,但value還在
- key是弱引用,
ThreadLocal對象被回收后,key變成null - 但value是強(qiáng)引用,還在
Entry中被引用著 - 如果線程不結(jié)束,這個(gè)value就永遠(yuǎn)無法被回收
2.線程結(jié)束,但 ThreadLocalMap 還在
Thread對象有ThreadLocalMap的引用Thread結(jié)束,Thread 對象可以被回收- 但如果
ThreadLocal對象還被其他地方引用,就可能導(dǎo)致ThreadLocalMap無法被完全回收
2. 線程池中的值污染
在線程池環(huán)境下,線程會被復(fù)用。如果上一個(gè)任務(wù)設(shè)置了ThreadLocal值但沒有清理,下一個(gè)任務(wù)可能會讀到錯誤的值。
// 線程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 任務(wù)1:用戶A的操作
executor.execute(() -> {
UserContext.setUserId("userA");
// 執(zhí)行業(yè)務(wù)邏輯...
// 忘記清理了!
});
// 任務(wù)2:用戶B的操作
executor.execute(() -> {
// 這里可能獲取到userA的ID!
String userId = UserContext.getUserId();
System.out.println("當(dāng)前用戶:" + userId); // 輸出:userA
});
3. 父子線程值傳遞問題
默認(rèn)情況下,子線程無法獲取父線程的 ThreadLocal 值。
public class ParentChildExample {
private static final ThreadLocal<String> holder = new ThreadLocal<>();
public static void main(String[] args) {
holder.set("父線程的值");
new Thread(() -> {
// 子線程獲取不到父線程的值
System.out.println("子線程:" + holder.get()); // 輸出:null
}).start();
}
}
ThreadLocal 問題的解決方案
方案1:一定要記得清理(最重要?。?/h3>
黃金法則:每次使用完ThreadLocal,一定要調(diào)用remove()方法。
public class SafeUserContext {
private static final ThreadLocal<User> context = new ThreadLocal<>();
public static void set(User user) {
context.set(user);
}
public static User get() {
return context.get();
}
public static void clear() {
context.remove(); // 清理!
}
}
// 使用try-finally確保一定會清理
public void processRequest(HttpServletRequest request) {
try {
User user = parseUser(request);
SafeUserContext.set(user);
// 執(zhí)行業(yè)務(wù)邏輯
doBusiness();
} finally {
// 無論是否發(fā)生異常,都會執(zhí)行清理
SafeUserContext.clear();
}
}
方案2:使用攔截器/過濾器統(tǒng)一管理
在Web應(yīng)用中,可以使用攔截器或過濾器統(tǒng)一管理ThreadLocal的生命周期。
@Component
public class ThreadLocalCleanInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
// 在請求開始時(shí)設(shè)置ThreadLocal
String traceId = request.getHeader("X-Trace-ID");
TraceContext.setTraceId(traceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// 請求結(jié)束后清理所有ThreadLocal
TraceContext.clear();
UserContext.clear();
PageContext.clear();
// ... 清理其他ThreadLocal
}
}
方案3:使用InheritableThreadLocal傳遞值
如果需要父子線程間傳遞值,可以使用InheritableThreadLocal。
public class InheritableContext {
private static final InheritableThreadLocal<String> context =
new InheritableThreadLocal<>();
public static void main(String[] args) {
context.set("父線程的值");
new Thread(() -> {
// 子線程可以獲取到父線程的值
System.out.println("子線程:" + context.get()); // 輸出:父線程的值
// 子線程修改值,不會影響父線程
context.set("子線程修改后的值");
}).start();
Thread.sleep(100);
System.out.println("父線程:" + context.get()); // 輸出:父線程的值
}
}
注意:線程池場景下慎用,因?yàn)榫€程是復(fù)用的,不是新創(chuàng)建的。
方案4:使用阿里開源的 TransmittableThreadLocal
對于線程池場景,可以考慮使用阿里的TransmittableThreadLocal,它是InheritableThreadLocal的增強(qiáng)版。
<!-- 添加依賴 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
public class TransmittableExample {
private static final TransmittableThreadLocal<String> context =
new TransmittableThreadLocal<>();
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
// 使用TtlExecutors包裝線程池
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(executor);
context.set("value-1");
ttlExecutor.execute(() -> {
System.out.println("任務(wù)1:" + context.get()); // 輸出:value-1
});
context.set("value-2");
ttlExecutor.execute(() -> {
System.out.println("任務(wù)2:" + context.get()); // 輸出:value-2
});
}
}
方案5:ThreadLocal的封裝
我們可以封裝一個(gè)安全的ThreadLocal工具類。
public class SafeThreadLocal<T> {
private final ThreadLocal<T> threadLocal = new ThreadLocal<>();
// 設(shè)置值,并返回一個(gè)Cleanup對象
public Cleanup set(T value) {
threadLocal.set(value);
return new Cleanup();
}
public T get() {
return threadLocal.get();
}
public void remove() {
threadLocal.remove();
}
// Cleanup類,用于自動清理
public class Cleanup implements AutoCloseable {
@Override
public void close() {
threadLocal.remove();
}
}
}
// 使用示例:try-with-resources自動清理
public void process() {
SafeThreadLocal<String> safeHolder = new SafeThreadLocal<>();
try (SafeThreadLocal<String>.Cleanup cleanup = safeHolder.set("value")) {
// 執(zhí)行業(yè)務(wù)邏輯
String value = safeHolder.get();
// ...
} // 這里會自動調(diào)用cleanup.close(),清理ThreadLocal
}
寫在最后
ThreadLocal 是Java并發(fā)編程中的一個(gè)重要工具,它解決了多線程環(huán)境下的數(shù)據(jù)隔離問題。但任何強(qiáng)大的工具一樣,它都需要被正確的使用。
該用ThreadLocal的時(shí)候:
- 需要在線程內(nèi)共享數(shù)據(jù),但不想用方法參數(shù)傳遞
- 需要保證線程安全的數(shù)據(jù)隔離
- 工具類需要線程安全但不想用同步鎖
看到這,相信你對 ThreadLocal 已經(jīng)不再是那么陌生了。
以上就是Java項(xiàng)目開發(fā)中ThreadLocal的6大用法總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于Java ThreadLocal用法的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
idea報(bào)錯:java程序包c(diǎn)om.github.xiaoymin.knife4j.spring.annotations
這篇文章主要介紹了idea報(bào)錯:java程序包c(diǎn)om.github.xiaoymin.knife4j.spring.annotations不存在問題解決,需要的朋友可以參考下2023-06-06
Java中的StampedLock實(shí)現(xiàn)原理詳解
這篇文章主要介紹了Java中的StampedLock實(shí)現(xiàn)原理詳解,ReentrantReadWriteLock采用悲觀讀,第一個(gè)讀線程拿到鎖后,第二個(gè)/第三個(gè)讀線程可以拿到鎖,特別是在讀線程很多,寫線程很少時(shí),需要的朋友可以參考下2024-01-01
聊聊Spring MVC JSON數(shù)據(jù)交互的問題
我們在開發(fā)中后端經(jīng)常需要接受來自于前端傳遞的Json字符串?dāng)?shù)據(jù),怎么把Json字符串轉(zhuǎn)換為Java對象呢?下面小編給大家?guī)砹薙pring MVC JSON數(shù)據(jù)交互的問題,感興趣的朋友一起看看吧2021-10-10
Java設(shè)計(jì)模式七大原則之合成復(fù)用原則詳解
合成復(fù)用原則(Composite Reuse Principle),即盡量使用組合/聚合的方式,而不是使用繼承。本文將為大家具體介紹一下Java設(shè)計(jì)模式七大原則之一的合成復(fù)用原則,需要的可以參考一下2022-02-02
MySQL數(shù)據(jù)文件直接通過拷貝備份與恢復(fù)的操作方法
這篇文章主要介紹了MySQL數(shù)據(jù)文件直接通過拷貝備份與恢復(fù)的操作方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09
netty-grpc一次DirectByteBuffer內(nèi)存泄露問題
這篇文章主要介紹了netty-grpc一次DirectByteBuffer內(nèi)存泄露問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12

