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

使用
- (是線程安全的) 在這個(gè)demo中,localStr是共享的,隨后在每個(gè)線程中給localStr設(shè)置值為自己線程的名字,然后再將當(dāng)前線程的日志輸出。
- sleep5毫秒是為了體現(xiàn)出是否存在線程安全問(wèn)題。
- 從運(yùn)行結(jié)果可以看到,是不存在線程安全問(wè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();
}
}
}

而對(duì)于普通變量來(lái)說(shuō),很明顯是存在線程安全問(wèn)題的:
/**
* @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.源碼解析 – 探究實(shí)現(xiàn)思路
threadLocals變量與ThreadLocalMap
- 每個(gè)線程的本地變量并不存放于ThreadLocal對(duì)象中,而是存在調(diào)用線程的threadLocals變量中。因?yàn)槭蔷€程對(duì)象的成員變量,所以生命周期等同于線程的生命周期。

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

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

3.InheritableThreadLocal與繼承性
InheritableThreadLocal英語(yǔ)翻譯一下就是可繼承的ThreadLocal,讓我們看下它和ThreadLocal的繼承性體現(xiàn)在哪。
這里的繼承性指的是:子線程是否能訪問(wèn)父線程的變量。
ThreadLocal的不可繼承性
threadLocals是當(dāng)前線程的成員變量,在子線程中不可見(jiàn)
/**
* @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線程為其設(shè)置的值");
new Thread(new Runnable() {
@Override
public void run() {
log.debug("訪問(wèn)localStr : " + localStr.get());
}
}).start();
System.out.println(localStr.get());
}
}

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

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

我們進(jìn)入?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)建出來(lái),當(dāng)前線程就是即將要?jiǎ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)
// 設(shè)置子線程中的inheritableThreadLocals設(shè)置為父線程的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();
}
我們重點(diǎn)看一下和inheritThreadLocals相關(guān)的地方(含注釋的地方)
- 在進(jìn)入init方法后,先獲取了父線程,然后再下面判斷了父線程的inheritThreadLocals 是否為空,不為空就調(diào)用ThreadLocal.createInheritedMap方法,參數(shù)就是父線程的inheritThreadLocals 。
再看下ThreadLocal.createInheritedMap方法:
- 調(diào)用了自身的構(gòu)造方法,將parentMap傳入。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
看下這個(gè)構(gòu)造方法:
- 發(fā)現(xiàn)主要是用parentMap的所有entry初始化當(dāng)前的map。
- 在注釋處,調(diào)用了inheritThreadLocals重寫的childValue方法,而重寫后,直接返回的是parentValue,也就是將父線程的inheritThreadLocal里面的entry完整的復(fù)制到了子線程中。
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) {
// 調(diào)用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++;
}
}
}
}
如何理解這個(gè)繼承性
通過(guò)上面的源碼分析,可以發(fā)現(xiàn),InheritableThreadLocal的繼承性主要體現(xiàn)在:創(chuàng)建子線程時(shí),會(huì)將父線程的inheritThreadLocals里面所有entry拷貝一份給子進(jìn)程。
那么當(dāng)子進(jìn)程被創(chuàng)建出來(lái)之后,父進(jìn)程又修改了inheritThreadLocals里面的值,這個(gè)操作是否對(duì)子線程可見(jiàn),通過(guò)上面的源碼可知,這個(gè)操作明顯是不可見(jiàn)的,下面有個(gè)demo可以證實(shí)。
- sleep操作是為了控制兩個(gè)線程的執(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設(shè)置的值");
new Thread(new Runnable() {
@Override
public void run() {
log.debug("子線程第一次訪問(wèn)inheritableLocalStr : " + inheritableLocalStr.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("子線程第二次訪問(wèn)inheritableLocalStr : " + inheritableLocalStr.get());
}
}).start();
Thread.sleep(500);
inheritableLocalStr.set("main線程第二次為inheritableLocalStr設(shè)置的值");
log.debug("main線程第二次為inheritableLocalStr賦值");
Thread.sleep(1000);
}
}
看下輸出:

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

使用強(qiáng)引用會(huì)如何?
現(xiàn)在假設(shè)Entry的key是一個(gè)對(duì)ThreadLocal的強(qiáng)引用,當(dāng)ThreadLocal對(duì)象使用完后,外部的強(qiáng)引用不存在,但是因?yàn)楫?dāng)前線程對(duì)象中的threadLocals還持有ThreadLocal的強(qiáng)引用,而threadLocals的生命周期是和線程一致的,這個(gè)時(shí)候,如果沒(méi)有手動(dòng)刪除,整個(gè)Entry就發(fā)生了內(nèi)存泄露。
使用弱引用會(huì)如何?
現(xiàn)在假設(shè)Entry的key是一個(gè)對(duì)ThreadLocal的弱引用,當(dāng)ThreadLocal對(duì)象使用完后,外部的強(qiáng)引用不存在,此時(shí)ThreadLocal對(duì)象只存在Entry中key對(duì)它的弱引用,在下次GC的時(shí)候,這個(gè)ThreadLocal對(duì)象就會(huì)被回收,導(dǎo)致key為null,此時(shí)value的強(qiáng)引用還存在,但是value已經(jīng)不會(huì)被使用了,如果沒(méi)有手動(dòng)刪除,那么這個(gè)Entry中的key就會(huì)發(fā)生內(nèi)存泄露。
使用弱引用還有一些好處,那就是,當(dāng)key為null時(shí), ThreadLocalMap中最多存在一個(gè)key為null,并且當(dāng)調(diào)用set(),get(),remove()這些方法的時(shí)候,是會(huì)清除掉key為null的entry的。
set()、get()、remove() 方法中相關(guān)實(shí)現(xiàn)
- 從下可以發(fā)現(xiàn),set方法首先會(huì)進(jìn)入一個(gè)循環(huán)。
- 在這個(gè)循環(huán)中,會(huì)遍歷整個(gè)Entry數(shù)組。直到遇到一個(gè)空的entry,退出循環(huán)。
- 當(dāng)遇到已存在的key'時(shí),會(huì)直接替換value,然后返回。
- 當(dāng)遇到key為空的entry的時(shí)候,會(huì)直接將當(dāng)前的entry存在這個(gè)過(guò)時(shí)的entry中,然后返回。
通過(guò)這個(gè)方法的源碼可以看出,key為null的那個(gè)entry實(shí)際上遲早會(huì)被替換成新的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;
// 替換過(guò)時(shí)的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)]) {
// 清除過(guò)時(shí)的key
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
總結(jié)
- ThreadLocal如果對(duì)ThreadLocalMap的key使用強(qiáng)引用,那么會(huì)存在整個(gè)entry發(fā)生內(nèi)存泄露的問(wèn)題,如果不手動(dòng)清除,那么這個(gè)不被使用的entry會(huì)一直存在。
- ThreadLocal如果對(duì)ThreadLocalMap的key使用弱引用,那么可能會(huì)存在一個(gè)entry的value發(fā)生內(nèi)存泄露,但是在調(diào)用set(),get(),remove() 方法時(shí),key為null的entry會(huì)被清除掉。
- 發(fā)生內(nèi)存泄露最根本的原因是:threadLocals的生命周期是和線程一致的。
- 每次使用完ThreadLocal對(duì)象后,必須調(diào)用它的remove()方法清除數(shù)據(jù)。
5.ThreadLocal應(yīng)用
ThreadLocal把數(shù)據(jù)存放到線程本地,解決了線程安全問(wèn)題,沒(méi)有使用鎖,直接訪問(wèn)線程本地變量,效率較高(空間換時(shí)間。)
同時(shí)threadLocals的生命周期是和線程一致的,可以解決很多參數(shù)傳遞問(wèn)題。
- Session管理(Mabaties使用ThreadLocal存儲(chǔ)session),數(shù)據(jù)庫(kù)連接。
- 如果需要跟蹤請(qǐng)求的整個(gè)流程,可以使用ThreadLocal來(lái)傳遞參數(shù)。
ATFWUS 2021-11-11
以上就是深入淺出解析Java ThreadLocal原理的詳細(xì)內(nèi)容,更多關(guān)于Java ThreadLocal原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot整合MongoDB實(shí)現(xiàn)文件上傳下載刪除
這篇文章主要介紹了SpringBoot整合MongoDB實(shí)現(xiàn)文件上傳下載刪除的方法,幫助大家更好的理解和學(xué)習(xí)使用SpringBoot框架,感興趣的朋友可以了解下2021-05-05
SpringBoot如何手寫一個(gè)starter并使用這個(gè)starter詳解
starter是SpringBoot中的一個(gè)新發(fā)明,它有效的降低了項(xiàng)目開(kāi)發(fā)過(guò)程的復(fù)雜程度,對(duì)于簡(jiǎn)化開(kāi)發(fā)操作有著非常好的效果,下面這篇文章主要給大家介紹了關(guān)于SpringBoot如何手寫一個(gè)starter并使用這個(gè)starter的相關(guān)資料,需要的朋友可以參考下2022-12-12
Java 中實(shí)現(xiàn)隨機(jī)無(wú)重復(fù)數(shù)字的方法
為了更好地理解這個(gè)題意,我們先來(lái)看下具體內(nèi)容:生成一個(gè)1-100 的隨機(jī)數(shù)組,但數(shù)組中的數(shù)字不能重復(fù),即位置是隨機(jī)的,但數(shù)組元素不能重復(fù)2013-03-03

