天天看點

我麻了,ThreadLocal竟然擷取不到值(大坑)

作者:Java合集

大家好,我是小趴菜,今天項目上測試環境,再給上司示範的時候出現了bug,很尴尬。于是我跟前端同學通過模拟請求,最後發現在調一個接口的時候傳回了一個 token為空 的錯誤。

但是前端同學說傳了token了,那為什麼還會報token為空的錯誤呢

1:問題起因

我們項目使用的JWT生成使用者token,每次請求都要經過攔截器校驗。因為在請求的時候我們經常需要用到目前使用者登入的ID,是以我們使用到了ThhreadLocal這個工具類。

Map<String, Claim> claimMap = JwtUtil.verifyToken(headerToken);
if (null == claimMap) {
    throw new GlobalException(ResponseEnums.TOKEN_INVALID_ERROR);
}
//将參數放入上下文中
Map<String, Object> result = new HashMap<>();
Set<Map.Entry<String, Claim>> entrySet = claimMap.entrySet();
for (Map.Entry<String, Claim> claimEntry : entrySet) {
    result.put(claimEntry.getKey(), claimEntry.getValue().asString());
}
//将使用者ID存到ThreadLocal中,以便後續的擷取使用
ThreadLocalUtil.getInstance().setContext(result);
複制代碼           

這裡也貼上ThreadLocalUtil工具類代碼

@Slf4j
public class ThreadLocalUtil {

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

    private ThreadLocalUtil() {}

    public static ThreadLocalUtil getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final ThreadLocalUtil INSTANCE = new ThreadLocalUtil();
    }

    public void setContext(Map<String, Object> map) {
        CONTEXT.set(map);
    }

    public static Map<String, Object> getContext() {
        return CONTEXT.get();
    }

    public void clear() {
        CONTEXT.remove();
    }

    public static Integer getUserId() {
        Map<String, Object> context = getContext();
        if(context == null || !context.containsKey(JwtUtil.USER_ID)) {
            throw new GlobalException(ResponseEnums.TOKEN_IS_NULL_ERROR);
        }
        return Integer.parseInt(String.valueOf(context.get(JwtUtil.USER_ID)));
    }

}
複制代碼           

2:問題複現

我們寫一個簡單的測試

public static void main(String[] args) {
    HashMap<String,Object> map = new HashMap<>();
    map.put(JwtUtil.USER_ID,"1");
    ThreadLocalUtil.getInstance().setContext(map);
    System.out.println(ThreadLocalUtil.getUserId());
}
複制代碼           
我麻了,ThreadLocal竟然擷取不到值(大坑)

可以看到可以拿到我們設定的值。

但是如果将ThreadLocal跟Java8的Stream一起配合使用呢?

public static void main(String[] args) {
    HashMap<String,Object> map = new HashMap<>();
    map.put(JwtUtil.USER_ID,"1");
    ThreadLocalUtil.getInstance().setContext(map);
    
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    //ThreadLocal擷取值放在stream裡面執行
    list.parallelStream().filter(x -> x.equals(ThreadLocalUtil.getUserId())).collect(Collectors.toList());
}
複制代碼           
我麻了,ThreadLocal竟然擷取不到值(大坑)

我們的問題複現了,報了token為空的錯誤。但是這還是随機會出現的情況,并不是每次都會出現。是以導緻我們在調試的時候并沒有出現這個問題

3:分析問題

咋一看并不能知道為什麼會這樣,是以我在擷取使用者id的列印了一下日志

我麻了,ThreadLocal竟然擷取不到值(大坑)

看出問題了吧,竟然有三個線程來擷取,因為我們設定值的線程就是main線程,是以前面二個線程擷取到的值就是空的,是以就抛出了異常

我麻了,ThreadLocal竟然擷取不到值(大坑)

是以現在隻需要知道這個 ForkJoinPool是在哪就好了,最終在翻看源碼,找到原來就是在jdk8的Stream裡面。

這是為什麼呢?因為Jdk8的Stream底層使用了ForkJoinPool線程池,這就導緻當我們調用 ThreadLocalUtil.getUserId()的時候,是直接送出到了ForkJoinPool線程池中去了,這時候就會有其它線程去調用這個方法,是以就拿不到值了

4:如何解決

解決辦法就很簡單了,隻需要把ThreadLocalUtil.getUserId()單獨拿出來執行就可以了

public static void main(String[] args) {
    HashMap<String,Object> map = new HashMap<>();
    map.put(JwtUtil.USER_ID,"1");
    ThreadLocalUtil.getInstance().setContext(map);

    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    //單獨拿出來執行
    Integer userId = ThreadLocalUtil.getUserId();
    list.parallelStream().filter(x -> x.equals(userId)).collect(Collectors.toList());
}
複制代碼           
我麻了,ThreadLocal竟然擷取不到值(大坑)

是以當你項目使用到了ThreadLocal的時候,切記要單獨使用,否則指不定就出現跟我一樣的問題了

繼續閱讀