深入淺出解析Java ThreadLocal原理
分享一下最近看的ThreadLocal的源碼的一些體會。
1.了解ThreadLocal
簡介
- ThreadLocal是JDK中java.lang包下提供的類。
- ThreadLocal是線程安全的,并且沒有使用到鎖。
- 常用來存放線程獨有變量,解決參數(shù)傳遞問題。
- 當我們創(chuàng)建一個ThreadLocal包裝的變量后,每個訪問這個變量的線程會在自己的線程空間創(chuàng)建這個變量的一個副本,在每次操作這個變量的時候,都是在自己的線程空間內操作,解決了線程安全問題。

使用
- (是線程安全的) 在這個demo中,localStr是共享的,隨后在每個線程中給localStr設置值為自己線程的名字,然后再將當前線程的日志輸出。
- sleep5毫秒是為了體現(xiàn)出是否存在線程安全問題。
- 從運行結果可以看到,是不存在線程安全問題的:
/**
* @author ATFWUS
* @version 1.0
* @date 2021/11/8 21:23
* @description
*/
@Slf4j
public class ThreadLocalTest {
static ThreadLocal<String> localStr = new ThreadLocal<>();
public static void main(String[] args) {
List<Thread> list = new LinkedList<>();
for(int i = 0; i < 1000; i++){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
localStr.set(Thread.currentThread().getName() + " localStr");
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(localStr.get());
}
}, "t" + String.valueOf(i));
list.add(t);
}
for (Thread t : list) {
t.start();
}
}
}

而對于普通變量來說,很明顯是存在線程安全問題的:
/**
* @author ATFWUS
* @version 1.0
* @date 2021/11/8 21:23
* @description
*/
@Slf4j
public class ThreadLocalTest {
static ThreadLocal<String> localStr = new ThreadLocal<>();
static String shareStr;
public static void main(String[] args) {
List<Thread> list = new LinkedList<>();
for(int i = 0; i < 1000; i++){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
shareStr = Thread.currentThread().getName() + " shareStr";
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(shareStr);
}
}, "t" + String.valueOf(i));
list.add(t);
}
for (Thread t : list) {
t.start();
}
}
}

2.源碼解析 – 探究實現(xiàn)思路
threadLocals變量與ThreadLocalMap
- 每個線程的本地變量并不存放于ThreadLocal對象中,而是存在調用線程的threadLocals變量中。因為是線程對象的成員變量,所以生命周期等同于線程的生命周期。

- 而threadLocals是ThreadLocalMap類的實例。
- ThreadLocalMap實際上是一個類似HashMap的實現(xiàn),是ThreadLocal的靜態(tài)內部類。
- 看下Doug Lea寫的注釋: ThreadLocalMap是一個定制的hash map,僅適用于維護線程本地值。在ThreadLocal類之外沒有暴露任何的操作。這個類是私有的,允許在類線程中聲明字段。為了處理非常大并長期存在(對象)的用法,哈希表的entries使用weakReference作為鍵。但是,由于沒有使用引用隊列,因此只有當表開始耗盡空間時,才能保證刪除過時的entries。

- 暫不探究ThreadLocalMap的內部實現(xiàn)細節(jié),暫時只需要知道實現(xiàn)了一個hash map,并且Entry的key是弱引用即可,具體的set() get() remove() 方法在下文中會有。
set(T value) 方法
- 進入set(T value) 方法后,先嘗試獲取map,如果獲取到了map,直接設置值,否則新建一個map。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
get() 方法
- 進入get()方法后,首先獲取當前線程,然后進入getMap(Thread t)中獲取ThreadLocalMap對象,直接返回t.threadLocals。
- 如果map不為空,直接返回map中當前ThreadLocal作為鍵對應的值。
- 如果map為空,需要先進行初始化。調用setInitialValue()方法進行初始化。
- setInitialValue()中先獲取一個初始值,默認為null。
- 如果map存在當前線程中,直接設置初始值。
- 如果map不存在當前線程中,需要先創(chuàng)建一個map。
- createMap(Thread t, T firstValue)中就是new了一個ThreadLocalMap對象,并且初始化了一個entry對。
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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
remove() 方法
- remove() 方法中,先判斷map是否存在,不存在直接將map中this作為鍵的entry刪掉。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
實現(xiàn)思路總結
- ThreadLocal搭配線程的threadLocals變量實現(xiàn),當調用set(T value) 和 get() 方法時,如果線程中的threadLocals仍然為null,會為其初始化。
- ThreadLocal對象往threadLocals存儲具體變量時,key是ThreadLocal對象的自身引用,value是真正的變量,且key是弱引用。

3.InheritableThreadLocal與繼承性
InheritableThreadLocal英語翻譯一下就是可繼承的ThreadLocal,讓我們看下它和ThreadLocal的繼承性體現(xiàn)在哪。
這里的繼承性指的是:子線程是否能訪問父線程的變量。
ThreadLocal的不可繼承性
threadLocals是當前線程的成員變量,在子線程中不可見
/**
* @author ATFWUS
* @version 1.0
* @date 2021/11/9 14:29
* @description
*/
@Slf4j
public class InheritableThreadLocalTest {
static ThreadLocal<String> localStr = new ThreadLocal<>();
public static void main(String[] args) {
localStr.set("main線程為其設置的值");
new Thread(new Runnable() {
@Override
public void run() {
log.debug("訪問localStr : " + localStr.get());
}
}).start();
System.out.println(localStr.get());
}
}

InheritableThreadLocal實現(xiàn)繼承性的源碼剖析
看一下InheritableThreadLocal的源碼:

源碼非常簡短,下面簡單分析一下:
- InheritableThreadLocal類繼承自ThreadLocal類,重寫了childValue(T parentValue)、getMap()、createMap(Thread t, T firstValue) 三個方法。
- createMap(Thread t, T firstValue)會在初始化的時候調用,重寫createMap(Thread t, T firstValue) 意味著,InheritableThreadLocal的實例使用的是線程對象中的inheritableThreadLocals,而不再是原來的threadLocals。
- getMap() 方法也是確保使用的是inheritableThreadLocals。
- childValue(T parentValue) 方法中,直接返回了parentValue,這個方法會在ThreadLocal的構造方法中被調用,為了弄清這個意圖,我們有必要看看Thread類初始化方法的源碼。
從Thread的構造方法看,發(fā)現(xiàn)所有的構造方法都會調用init()方法進行初始化,init()方法有兩個重載形式。

我們進入?yún)?shù)較多的init方法查看一下:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
// 新線程還未創(chuàng)建出來,當前線程就是即將要創(chuàng)建線程的父線程
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
// 如果父線程的inheritThreadLocals 不為空
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 設置子線程中的inheritableThreadLocals設置為父線程的inheritableThreadLocals
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
我們重點看一下和inheritThreadLocals相關的地方(含注釋的地方)
- 在進入init方法后,先獲取了父線程,然后再下面判斷了父線程的inheritThreadLocals 是否為空,不為空就調用ThreadLocal.createInheritedMap方法,參數(shù)就是父線程的inheritThreadLocals 。
再看下ThreadLocal.createInheritedMap方法:
- 調用了自身的構造方法,將parentMap傳入。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
看下這個構造方法:
- 發(fā)現(xiàn)主要是用parentMap的所有entry初始化當前的map。
- 在注釋處,調用了inheritThreadLocals重寫的childValue方法,而重寫后,直接返回的是parentValue,也就是將父線程的inheritThreadLocal里面的entry完整的復制到了子線程中。
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 調用inheritThreadLocals重寫的childValue方法
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
如何理解這個繼承性
通過上面的源碼分析,可以發(fā)現(xiàn),InheritableThreadLocal的繼承性主要體現(xiàn)在:創(chuàng)建子線程時,會將父線程的inheritThreadLocals里面所有entry拷貝一份給子進程。
那么當子進程被創(chuàng)建出來之后,父進程又修改了inheritThreadLocals里面的值,這個操作是否對子線程可見,通過上面的源碼可知,這個操作明顯是不可見的,下面有個demo可以證實。
- sleep操作是為了控制兩個線程的執(zhí)行流程。
/**
* @author ATFWUS
* @version 1.0
* @date 2021/11/9 14:29
* @description
*/
@Slf4j
public class InheritableThreadLocalTest {
static ThreadLocal<String> localStr = new ThreadLocal<>();
static InheritableThreadLocal<String> inheritableLocalStr = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
inheritableLocalStr.set("main線程第一次為inheritableLocalStr設置的值");
new Thread(new Runnable() {
@Override
public void run() {
log.debug("子線程第一次訪問inheritableLocalStr : " + inheritableLocalStr.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("子線程第二次訪問inheritableLocalStr : " + inheritableLocalStr.get());
}
}).start();
Thread.sleep(500);
inheritableLocalStr.set("main線程第二次為inheritableLocalStr設置的值");
log.debug("main線程第二次為inheritableLocalStr賦值");
Thread.sleep(1000);
}
}
看下輸出:

可以發(fā)現(xiàn),子線程創(chuàng)建出來后,對父線程中inheritThreadLocals的修改操作,對子線程不可見。
總結
- ThreadLocal不可繼承,threadLocals是當前線程的成員變量,在子線程中不可見。
- InheritableThreadLocal可繼承,原理是:在新建子線程的時候,將父線程中inheritThreadLocals所有的entry拷貝給了子線程。
- 子線程創(chuàng)建出來后,對父線程中inheritThreadLocals的修改操作,對子線程不可見。
4.存在的內存泄露問題
要充分理解ThreadLocal中存在的內存泄露問題,需要有以下JVM對內存管理的前置知識(這里篇幅問題就不補充了):
- 什么是內存泄露?
- 什么是強引用?
- 什么是弱引用?
- 何時GC?
- 強引用和弱引用GC時的區(qū)別?
在分析上述ThreadLocalMap源碼的時候,注意到有一個小細節(jié),ThreadLocalMap的Entry繼承了WeakReference<ThreadLocal<?>>,也就是說Entry的key是一個對ThreadLocal<?>的弱引用。問題來了,為什么這里要使用弱引用呢?

使用強引用會如何?
現(xiàn)在假設Entry的key是一個對ThreadLocal的強引用,當ThreadLocal對象使用完后,外部的強引用不存在,但是因為當前線程對象中的threadLocals還持有ThreadLocal的強引用,而threadLocals的生命周期是和線程一致的,這個時候,如果沒有手動刪除,整個Entry就發(fā)生了內存泄露。
使用弱引用會如何?
現(xiàn)在假設Entry的key是一個對ThreadLocal的弱引用,當ThreadLocal對象使用完后,外部的強引用不存在,此時ThreadLocal對象只存在Entry中key對它的弱引用,在下次GC的時候,這個ThreadLocal對象就會被回收,導致key為null,此時value的強引用還存在,但是value已經不會被使用了,如果沒有手動刪除,那么這個Entry中的key就會發(fā)生內存泄露。
使用弱引用還有一些好處,那就是,當key為null時, ThreadLocalMap中最多存在一個key為null,并且當調用set(),get(),remove()這些方法的時候,是會清除掉key為null的entry的。
set()、get()、remove() 方法中相關實現(xiàn)
- 從下可以發(fā)現(xiàn),set方法首先會進入一個循環(huán)。
- 在這個循環(huán)中,會遍歷整個Entry數(shù)組。直到遇到一個空的entry,退出循環(huán)。
- 當遇到已存在的key'時,會直接替換value,然后返回。
- 當遇到key為空的entry的時候,會直接將當前的entry存在這個過時的entry中,然后返回。
通過這個方法的源碼可以看出,key為null的那個entry實際上遲早會被替換成新的entry。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
// 發(fā)現(xiàn)key為空
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
同理,可以看到在get方法中也存在:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
// 替換過時的entry
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
remove() 方法中也是一樣:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 清除過時的key
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
總結
- ThreadLocal如果對ThreadLocalMap的key使用強引用,那么會存在整個entry發(fā)生內存泄露的問題,如果不手動清除,那么這個不被使用的entry會一直存在。
- ThreadLocal如果對ThreadLocalMap的key使用弱引用,那么可能會存在一個entry的value發(fā)生內存泄露,但是在調用set(),get(),remove() 方法時,key為null的entry會被清除掉。
- 發(fā)生內存泄露最根本的原因是:threadLocals的生命周期是和線程一致的。
- 每次使用完ThreadLocal對象后,必須調用它的remove()方法清除數(shù)據(jù)。
5.ThreadLocal應用
ThreadLocal把數(shù)據(jù)存放到線程本地,解決了線程安全問題,沒有使用鎖,直接訪問線程本地變量,效率較高(空間換時間。)
同時threadLocals的生命周期是和線程一致的,可以解決很多參數(shù)傳遞問題。
- Session管理(Mabaties使用ThreadLocal存儲session),數(shù)據(jù)庫連接。
- 如果需要跟蹤請求的整個流程,可以使用ThreadLocal來傳遞參數(shù)。
ATFWUS 2021-11-11
以上就是深入淺出解析Java ThreadLocal原理的詳細內容,更多關于Java ThreadLocal原理的資料請關注腳本之家其它相關文章!
相關文章
SpringBoot整合MongoDB實現(xiàn)文件上傳下載刪除
這篇文章主要介紹了SpringBoot整合MongoDB實現(xiàn)文件上傳下載刪除的方法,幫助大家更好的理解和學習使用SpringBoot框架,感興趣的朋友可以了解下2021-05-05
SpringBoot如何手寫一個starter并使用這個starter詳解
starter是SpringBoot中的一個新發(fā)明,它有效的降低了項目開發(fā)過程的復雜程度,對于簡化開發(fā)操作有著非常好的效果,下面這篇文章主要給大家介紹了關于SpringBoot如何手寫一個starter并使用這個starter的相關資料,需要的朋友可以參考下2022-12-12

