我是如何用 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 底層原理是在線程內部維護一個 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();
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM