天天看點

如何避免忘記清理 ThreadLocal ?一、背景二、解法三、一些疑問四、總結

一、背景

ThreadLocal 可以解決“線程安全問題”。

如何避免忘記清理 ThreadLocal ?一、背景二、解法三、一些疑問四、總結

也可以作為上下文暫存資料以備後續步驟擷取。

但是 ThreadLocal 用不好的确容易産生故障,因而有些團隊不允許使用 ThreadLocal。

最核心的一個原因是很容易忘記清理,線上程池環境下複用導緻串環境。

如何避免忘記清理 ThreadLocal ?一、背景二、解法三、一些疑問四、總結

那麼,有什麼優雅的解法沒?本文給出自己的一個解法。

二、解法

package basic.thread;

import com.alibaba.ttl.TransmittableThreadLocal;

import java.util.HashMap;
import java.util.Map;

public class ThreadContext {

    private static final ThreadLocal<Map<String, Object>> CONTEXT = new TransmittableThreadLocal<>();

    /**
     * 初始化上下文
     */
    public static void initContext() {
        Map<String, Object> con = CONTEXT.get();
        if (con == null) {

            CONTEXT.set(new HashMap<>(8));
        } else {
            CONTEXT.get().clear();
        }
    }

    /**
     * 清除上下文
     */
    public static void clearContext() {
        CONTEXT.remove();
    }

    /**
     * 擷取上下文内容
     */
    public static <T> T getValue(String key) {
        Map<String, Object> con = CONTEXT.get();
        if (con == null) {
            return null;
        }
        return (T) con.get(key);
    }

    /**
     * 設定上下文參數
     */
    public static void putValue(String key, Object value) {
        Map<String, Object> con = CONTEXT.get();
        if (con == null) {
            CONTEXT.set(new HashMap<>(8));
            con = CONTEXT.get();
        }
        con.put(key, value);
    }
}

           

2.1 Java 開發手冊中的建議

如何避免忘記清理 ThreadLocal ?一、背景二、解法三、一些疑問四、總結

寫入如下:

public Result<R> executeAbility(T ability) {
           //初始化上下文
            ThreadContext.initContext();
        try {
            //省略核心業務代碼

        } finally {
            ThreadContext.clearContext();
        }
    }
           

2.2 進一步改進

相信絕大多數人會止步于此,但我認為這還是不夠的。

如何才能避免忘掉清理 threadlocal 呢?

JDK 源碼中有沒有類似的案例呢?

想想IO 讀寫檔案後,也是需要采用類似的做法去釋放資源,JDK 提供了 try-with-resource 讓釋放資源更簡單,使用者不需要手動寫 finnaly 去釋放資源。

普通案例:

如何避免忘記清理 ThreadLocal ?一、背景二、解法三、一些疑問四、總結

使用 try-with-resource

如何避免忘記清理 ThreadLocal ?一、背景二、解法三、一些疑問四、總結

另外我們知道,可以通過實作 AutoCloseable 來自定義 try-with-resource 的資源。

但最後發現并不是很适配,因為在傳遞上下文這種場景下, ThreadLocal 工具類通常都是靜态的,而且即使不适用靜态,擷取屬性時還要将該對象傳遞下去,不是很友善。

當然,如果大家不想以靜态的方式使用,也可以考慮實作 AutoClosebale 接口,使用 try-with-resource 的機制。

我們是否也可以采用類似的機制呢?

可以直接将初始化和清理方法私有化,提供無參和帶傳回值的封裝,使用 Runnbale 和 Callable 将調用作為參數傳入,在封裝的方法中封裝 try- finally 邏輯。

package basic.thread;

import com.alibaba.ttl.TransmittableThreadLocal;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;

public class ThreadContext {

    private static final ThreadLocal<Map<String, Object>> CONTEXT = new TransmittableThreadLocal<>();

    /**
     * 初始化上下文
     */
    private static void initContext() {
        Map<String, Object> con = CONTEXT.get();
        if (con == null) {

            CONTEXT.set(new HashMap<>(8));
        } else {
            CONTEXT.get().clear();
        }
    }

    /**
     * 清除上下文
     */
    private static void clearContext() {
        CONTEXT.remove();
    }

    /**
     * 擷取上下文内容
     */
    public static <T> T getValue(String key) {
        Map<String, Object> con = CONTEXT.get();
        if (con == null) {
            return null;
        }
        return (T) con.get(key);
    }

    /**
     * 設定上下文參數
     */
    public static void putValue(String key, Object value) {
        Map<String, Object> con = CONTEXT.get();
        if (con == null) {
            CONTEXT.set(new HashMap<>(8));
            con = CONTEXT.get();
        }
        con.put(key, value);
    }

    /**
     * 自動回收的封裝
     */
    public static void  runWithAutoClear(Runnable runnable){
        initContext();
        try{
            runnable.run();
        }finally{
            CONTEXT.remove();
        }
    }

    /**
     * 自動回收的封裝
     */
    public static <T> T callWithAutoClear(Callable<T> callable){
        initContext();
        try{
            try {
                return callable.call();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }finally{
            CONTEXT.remove();
        }
    }
}

           

使用參考:

public Result<R> executeAbility(T ability) {
      return  ThreadContext.callWithAutoClear(()->{
        		 // 業務核心代碼
       });
    }
           

2.3 切面

定義支援上下文注解,如

@EnableThreadContext

然後對該注解進行切面,執行前初始化上下文,執行後清理上下文即可。

具體代碼省略。

三、一些疑問

3.1 通常線程上下文會跨類使用,是以這麼做是不是沒意義?

通常線程上下文工具類套在需要使用該上下文工具的最外層即可。也可以直接套在 RPC 的接口實作層或者 Controller 的方法上。

整個調用如果涉及多個類,隻要在同一個線程中或者由同一個線程發起(使用

TransmittableThreadLocal

),子函數或者線程調用的方法中依然可以使用 ThreadContext 的 put 或者 get 方法。

四、總結

隻要思想不滑坡,辦法總比困難多。

我們應該想辦法去解決問題,而不是你回避問題。

當看到有些解決方案仍然容易出錯時,應該想辦法去做進一步的改進。