天天看點

我是如何用 ThreadLocal 虐面試官的?

我是陳皮,一個在網際網路 Coding 的 ITer,微信搜尋「陳皮的JavaLib」第一時間閱讀最新文章,回複【資料】,即可獲得我精心整理的技術資料,電子書籍,一線大廠面試資料和優秀履歷模闆。

ThreadLocal 簡介

Threadlocal 類提供了

線程局部變量

功能。意思可以在指定線程内部存儲資料,并且哪個線程存儲的資料隻能線程它自己有權限取得。

底層原理其實是線上程内部維護一個 Map 變量,然後 Threadlocal 對象作為 key,要存儲的資料作為 value。而 Threadlocal 類作為一個設定和通路這個線程局部變量的入口。

Threadlocal 對象一般定義為私有靜态的,而且通過它的 get 和 set 方法設定和擷取線程局部變量。

private static final ThreadLocal<UserContext> THREAD_LOCAL = new ThreadLocal<>();
           

如何使用 ThreadLocal

ThreadLocal 使用方法很簡單,它提供了三個公開的方法供外部調用。

  • void set(T value):設定線程局部變量
  • T get():擷取線程局部變量
  • void remove():删除線程局部變量
package com.chenpi;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/6/27
 * @Version 1.0
 */
public class ThreadLocalTest {

    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    public static void main(String[] args) {
        // 設定線程局部變量
        THREAD_LOCAL.set("我是陳皮,個人公衆号【陳皮的JavaLib】");
        // 使用線程局部變量
        peelChenpi();
        // 删除線程局部變量
        THREAD_LOCAL.remove();
        // 使用線程局部變量
        peelChenpi();
    }

    public static void peelChenpi() {
        System.out.println(THREAD_LOCAL.get());
    }
}

// 輸出結果
我是陳皮,個人公衆号【陳皮的JavaLib】
null
           

ThreadLocal 源碼分析

我是如何用 ThreadLocal 虐面試官的?

ThreadLocal 底層原理是線上程内部維護一個 Map 變量,然後 Threadlocal 對象作為 key,要存儲的資料作為 value。而 Threadlocal 類作為一個設定和通路這個線程局部變量的入口。

Thread 類中定義了一個

ThreadLocalMap

類型的變量 threadLocals,每個線程都有自己專屬的 threadLocals 變量,ThreadLocalMap 類是由 ThreadLocal 維護的一個靜态内部類。

ThreadLocal.ThreadLocalMap threadLocals = null;
           

Thread 的 threadLocals 變量是預設通路權限的,隻能被同個包下的類通路,是以我們是不能直接使用 Thread 的 threadLocals 變量的,這也就是為什麼能控制不同線程隻能擷取自己的資料,達到了線程隔離。Threadlocal 類是通路它的入口。

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
           

ThreadLocal 類中的靜态内部類 ThreadLocalMap 部分源碼如下,底層是維護的了一個 Entry 類型數組 table。

static class ThreadLocalMap {

        // Map中的Entry對象,弱引用類型,key是ThreadLocal對象,value是線程局部變量
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    
        // 初始化容量16,必須是2的幂次方
        private static final int INITIAL_CAPACITY = 16;
    
        // 存儲資料的數組,可擴容,長度必須是2的幂次方
        private Entry[] table;

        // table數組的大小
        private int size = 0;

        // table數組的門檻值,達到則擴容
        private int threshold; // Default to 0
        
}
           

為什麼 ThreadLocalMap 内部存儲機構是維護一個數組呢?因為一個線程是可以通過多個不同的 ThreadLocal 對象來設定多個線程局部變量的,這些局部變量都是存儲在自己線程的同一個 ThreadLocalMap 對象中。通過不同的 ThreadLocal 對象可以取得目前線程的不同局部變量值。

package com.chenpi;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/6/27
 * @Version 1.0
 */
public class ThreadLocalTest {

    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    private static final ThreadLocal<String> THREAD_LOCAL01 = new ThreadLocal<>();

    public static void main(String[] args) {
        THREAD_LOCAL.set("我是陳皮");
        System.out.println(THREAD_LOCAL.get());

        THREAD_LOCAL01.set("陳皮是我");
        System.out.println(THREAD_LOCAL01.get());
    }
}
           

那同一個線程的 ThreadLocalMap 對象的數組 table,目前線程的不同 ThreadLocal 是如何确定數組下标,如果數組下标沖突又是怎麼解決的呢?其實它不同于 HashMap 底層數組+連結清單+紅黑樹的存儲結構,它隻有 Entry 數組。

ThreadLocal 有個靜态的初始哈希值

nextHashCode

,然後每建立一個 ThreadLocal 對象都會在此哈希值的基礎上自增一次,自增量為0x61c88647。

// 每 new 一個 ThreadLocal 對象都會自增一次哈希值
private final int threadLocalHashCode = nextHashCode();

// 初始哈希值,靜态變量
private static AtomicInteger nextHashCode =
    new AtomicInteger();

// 自增量
private static final int HASH_INCREMENT = 0x61c88647;

// 自增一次
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
           

然後計算 table 數組下标是通過以下算法确定的,如果下标沖突,則下标會往後挪一位繼續判斷,直到不沖突為止。

// 首次建立 ThreadLocalMap 對象時,第一個元素的下标計算
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 後續元素的下标計算
int i = key.threadLocalHashCode & (len-1);
// 下标沖突時計算下一個下标的方法
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
           

我們看 ThreadLocal 類的 set 方法源碼,它是設定線程局部變量的入口方法,實作原理也很簡單。

  • 首先擷取目前線程的 ThreadLocalMap 變量
  • 如果 ThreadLocalMap 變量存在,則将 ThreadLocal 對象和 T 資料以鍵值對的形式存儲到 ThreadLocalMap 變量中
  • 如果 ThreadLocalMap 變量不存在,則建立 ThreadLocalMap 變量并綁定到目前線程中,再将 ThreadLocal 對象和 T 資料以鍵值對的形式存儲到 ThreadLocalMap 變量中
// 設定線程局部變量
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
           

ThreadLocal 類的 get 方法,它是通路線程局部變量的入口方法,實作原理也很簡單。

  • 首先擷取目前線程的 ThreadLocalMap 變量
  • 如果 ThreadLocalMap 變量存在,則将 ThreadLocal 對象作為 key,在 ThreadLocalMap 變量中查找對應的線程局部變量
  • 如果 ThreadLocalMap 變量不存在,則建立 ThreadLocalMap 變量并綁定到目前線程中,再将 ThreadLocal 對象和 null 以鍵值對的形式存儲到 ThreadLocalMap 變量中
// 通路線程局部變量
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}
           

ThreadLocal 類的 remove 方法,直接清除線程中 ThreadLocalMap 對象中以目前 ThreadLocal 對象為 key 的 Entry對象。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
           

你是否發現,ThreadLocal 類中的所有方法都是沒有加鎖的,因為 ThreadLocal 最終操作的都是對目前線程的 ThreadLocalMap 對象進行操作,既然線程處理自己的局部變量,就肯定不會有線程安全問題。

注意,同一個 ThreadLocal 變量在父線程中被設定值後,在子線程中是擷取這個值的。即不具備繼承性。具有繼承性的是 InheritableThreadLocal 類,下期文章再講解這個。

ThreadLocal 應用

ThreadLocal 具有線程隔離,線程安全的效果,如果資料是以線程為作用域并且不同線程具有不同的資料的時候,采用 ThreadLocal 是個不錯的選擇。

例如對于要使用者登入的服務,對于每一個請求,我們可能需要校驗使用者是否登入,以及在登入後,後續的請求中會使用到使用者資訊,那我們就可以将登入校驗過的使用者資訊放入線程局部變量中。

首先定義一個使用者資訊類,存放使用者登入校驗過的使用者資訊。

package com.chenpi;

import lombok.Data;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/6/27
 * @Version 1.0
 */
@Data
public class UserContext {

    private String userId;
    private String userName;
}
           

定義一個持有使用者資訊的管理工具類,主要使用者管理目前線程的使用者資訊。

package com.chenpi;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/6/27
 * @Version 1.0
 */
public class UserContextHolder {

    private static final ThreadLocal<UserContext> THREAD_LOCAL = new ThreadLocal<>();

    private UserContextHolder() {}

    public static void setUserContext(UserContext userContext) {
        THREAD_LOCAL.set(userContext);
    }

    public static UserContext getUserContext() {
        return THREAD_LOCAL.get();
    }

    public static void removeUserContext() {
        THREAD_LOCAL.remove();
    }
}
           

對需要使用者權限的接口進行攔截,然後将使用者資訊存儲到目前線程内部。注意,當請求完成後,需要将使用者資訊進行清除,避免記憶體洩露問題。

package com.chenpi;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * @Description 使用者權限驗證攔截
 * @Author 陳皮
 * @Date 2021/6/27
 * @Version 1.0
 */
@Component
public class UserPermissionInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
            Object handler) {

        if (handler instanceof HandlerMethod) {

            HandlerMethod handlerMethod = (HandlerMethod) handler;

            // 擷取使用者權限校驗注解
            UserAuthenticate userAuthenticate =
                    handlerMethod.getMethod().getAnnotation(UserAuthenticate.class);
            if (null == userAuthenticate) {
                userAuthenticate = handlerMethod.getMethod().getDeclaringClass()
                        .getAnnotation(UserAuthenticate.class);
            }
            if (userAuthenticate != null && userAuthenticate.permission()) {
                // 驗證使用者資訊
                UserContext userContext = userContextManager.getUserContext(request);
                // 将使用者資訊存儲到線程内部
                UserContextHolder.setUserContext(userContext);
            }
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
            Object handler, @Nullable Exception ex) {
        // 請求完後,清除目前線程的使用者資訊,避免記憶體洩露和使用者資訊混亂
        UserContextHolder.removeUserContext();
    }
}
           

至此,我們就能在目前請求的同一線程内,不用通過方法參數顯示傳遞使用者資訊,可以通過工具類随時随地擷取到目前使用者資訊了。

而且你會發現,如果方法調用鍊 A - B - C,AB 不需要使用者資訊,C 需要使用者資訊,那你需要層層通過方法參數傳遞使用者資訊。而使用 ThreadLocal 後,不用通過方法參數層層傳遞使用者資訊,避免了依賴污染,代碼也更加簡潔。

package com.chenpi;

import org.springframework.stereotype.Service;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/6/27
 * @Version 1.0
 */
@Service
public class UserService {

    public void chenPiDeJavaLib() {
        UserContext userContext = UserContextHolder.getUserContext();
    }
}