我是陳皮,一個在網際網路 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();
}
}