最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java項(xiàng)目開發(fā)中ThreadLocal的6大用法總結(jié)

 更新時(shí)間:2026年01月26日 09:28:52   作者:程序員大華  
ThreadLocal在項(xiàng)目中非常常用,本文整理了Java項(xiàng)目開發(fā)中ThreadLocal的6大常見用法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下

前言

你的系統(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)文章

  • Java線程中斷及線程中斷的幾種使用場景小結(jié)

    Java線程中斷及線程中斷的幾種使用場景小結(jié)

    在并發(fā)編程中,合理使用線程中斷機(jī)制可以提高程序的魯棒性和可維護(hù)性,本文主要介紹了Java線程中斷及線程中斷的幾種使用場景小結(jié),具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-01-01
  • Spring boot怎么整合Mybatis

    Spring boot怎么整合Mybatis

    spring boot的簡配置方便的開發(fā),下面通過本文給大家分享Spring boot整合Mybatis的方法,需要的朋友參考下
    2017-07-07
  • Java 8 核心新特性實(shí)戰(zhàn)指南

    Java 8 核心新特性實(shí)戰(zhàn)指南

    Lambda表達(dá)式是Java 8最核心的特性,它允許你將代碼視為數(shù)據(jù),使代碼更加簡潔、可讀,本教程將帶你深入掌握 Java 8 的核心特性,并通過實(shí)戰(zhàn)示例讓你快速上手,感興趣的朋友跟隨小編一起看看吧
    2026-04-04
  • java?jpa如何自定義sql語句

    java?jpa如何自定義sql語句

    這篇文章主要介紹了java?jpa如何自定義sql語句方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • 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

    這篇文章主要介紹了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)原理詳解

    這篇文章主要介紹了Java中的StampedLock實(shí)現(xiàn)原理詳解,ReentrantReadWriteLock采用悲觀讀,第一個(gè)讀線程拿到鎖后,第二個(gè)/第三個(gè)讀線程可以拿到鎖,特別是在讀線程很多,寫線程很少時(shí),需要的朋友可以參考下
    2024-01-01
  • 聊聊Spring MVC JSON數(shù)據(jù)交互的問題

    聊聊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ù)用原則詳解

    Java設(shè)計(jì)模式七大原則之合成復(fù)用原則詳解

    合成復(fù)用原則(Composite Reuse Principle),即盡量使用組合/聚合的方式,而不是使用繼承。本文將為大家具體介紹一下Java設(shè)計(jì)模式七大原則之一的合成復(fù)用原則,需要的可以參考一下
    2022-02-02
  • MySQL數(shù)據(jù)文件直接通過拷貝備份與恢復(fù)的操作方法

    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)存泄露問題

    這篇文章主要介紹了netty-grpc一次DirectByteBuffer內(nèi)存泄露問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-12-12

最新評論

洱源县| 元阳县| 鄱阳县| 华池县| 阜新| 靖西县| 宾阳县| 江西省| 炎陵县| 合作市| 于都县| 威信县| 泾源县| 泌阳县| 桃园县| 商水县| 且末县| 鹤岗市| 池州市| 邵阳县| 利辛县| 温州市| 长子县| 仁布县| 肇庆市| 彰化市| 梁山县| 广元市| 上饶市| 观塘区| 南昌县| 青铜峡市| 扬中市| 南京市| 宁南县| 柘荣县| 平安县| 通城县| 军事| 芜湖县| 紫阳县|