分享一下最近看的ThreadLocal的原始碼的一些體會,
文章目錄
- 1.了解ThreadLocal
- 簡介
- 使用(是執行緒安全的)
- 2.原始碼決議 -- 探究實作思路
- threadLocals變數與ThreadLocalMap
- set(T value) 方法
- get() 方法
- remove() 方法
- 實作思路總結
- 3.InheritableThreadLocal與繼承性
- ThreadLocal的不可繼承性
- InheritableThreadLocal實作繼承性的原始碼剖析
- 如何理解這個繼承性
- 總結
- 4.存在的記憶體泄露問題
- 使用強參考會如何?
- 使用弱參考會如何?
- set()、get()、remove() 方法中相關實作
- 總結
- 5.ThreadLocal應用
1.了解ThreadLocal
簡介
- ThreadLocal是JDK中java.lang包下提供的類,
- ThreadLocal是執行緒安全的,并且沒有使用到鎖,
- 常用來存放執行緒獨有變數,解決引數傳遞問題,
- 當我們創建一個ThreadLocal包裝的變數后,每個訪問這個變數的執行緒會在自己的執行緒空間創建這個變數的一個副本,在每次操作這個變數的時候,都是在自己的執行緒空間內操作,解決了執行緒安全問題,

使用(是執行緒安全的)
- 在這個demo中,localStr是共享的,隨后在每個執行緒中給localStr設定值為自己執行緒的名字,然后再將當前執行緒的日志輸出,
- sleep5毫秒是為了體現出是否存在執行緒安全問題,
- 從運行結果可以看到,是不存在執行緒安全問題的:
/**
* @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.原始碼決議 – 探究實作思路
threadLocals變數與ThreadLocalMap
- 每個執行緒的本地變數并不存放于ThreadLocal物件中,而是存在呼叫執行緒的threadLocals變數中,因為是執行緒物件的成員變數,所以生命周期等同于執行緒的生命周期,

- 而threadLocals是ThreadLocalMap類的實體,
- ThreadLocalMap實際上是一個類似HashMap的實作,是ThreadLocal的靜態內部類,
- 看下Doug Lea寫的注釋:
- ThreadLocalMap是一個定制的hash map,僅適用于維護執行緒本地值,在ThreadLocal類之外沒有暴露任何的操作,這個類是私有的,允許在類執行緒中宣告欄位,為了處理非常大并長期存在(物件)的用法,哈希表的entries使用weakReference作為鍵,但是,由于沒有使用參考佇列,因此只有當表開始耗盡空間時,才能保證洗掉過時的entries,

- ThreadLocalMap是一個定制的hash map,僅適用于維護執行緒本地值,在ThreadLocal類之外沒有暴露任何的操作,這個類是私有的,允許在類執行緒中宣告欄位,為了處理非常大并長期存在(物件)的用法,哈希表的entries使用weakReference作為鍵,但是,由于沒有使用參考佇列,因此只有當表開始耗盡空間時,才能保證洗掉過時的entries,
- 暫不探究ThreadLocalMap的內部實作細節,暫時只需要知道實作了一個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不存在當前執行緒中,需要先創建一個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);
}
實作思路總結
- ThreadLocal搭配執行緒的threadLocals變數實作,當呼叫set(T value) 和 get() 方法時,如果執行緒中的threadLocals仍然為null,會為其初始化,
- ThreadLocal物件往threadLocals存盤具體變數時,key是ThreadLocal物件的自身參考,value是真正的變數,且key是弱參考,

3.InheritableThreadLocal與繼承性
InheritableThreadLocal英語翻譯一下就是可繼承的ThreadLocal,讓我們看下它和ThreadLocal的繼承性體現在哪,
這里的繼承性指的是:子執行緒是否能訪問父執行緒的變數,
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實作繼承性的原始碼剖析
看一下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的構造方法看,發現所有的構造方法都會呼叫init()方法進行初始化,init()方法有兩個多載形式,

我們進入引數較多的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;
// 新執行緒還未創建出來,當前執行緒就是即將要創建執行緒的父執行緒
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方法,引數就是父執行緒的inheritThreadLocals ,
再看下ThreadLocal.createInheritedMap方法:
- 呼叫了自身的構造方法,將parentMap傳入,
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
看下這個構造方法:
- 發現主要是用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++;
}
}
}
}
如何理解這個繼承性
通過上面的原始碼分析,可以發現,InheritableThreadLocal的繼承性主要體現在:創建子執行緒時,會將父執行緒的inheritThreadLocals里面所有entry拷貝一份給子行程,
那么當子行程被創建出來之后,父行程又修改了inheritThreadLocals里面的值,這個操作是否對子執行緒可見,通過上面的原始碼可知,這個操作明顯是不可見的,下面有個demo可以證實,
- sleep操作是為了控制兩個執行緒的執行流程,
/**
* @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);
}
}
看下輸出:

可以發現,子執行緒創建出來后,對父執行緒中inheritThreadLocals的修改操作,對子執行緒不可見,
總結
- ThreadLocal不可繼承,threadLocals是當前執行緒的成員變數,在子執行緒中不可見,
- InheritableThreadLocal可繼承,原理是:在新建子執行緒的時候,將父執行緒中inheritThreadLocals所有的entry拷貝給了子執行緒,
- 子執行緒創建出來后,對父執行緒中inheritThreadLocals的修改操作,對子執行緒不可見,
4.存在的記憶體泄露問題
要充分理解ThreadLocal中存在的記憶體泄露問題,需要有以下JVM對記憶體管理的前置知識(這里篇幅問題就不補充了):
- 什么是記憶體泄露?
- 什么是強參考?
- 什么是弱參考?
- 何時GC?
- 強參考和弱參考GC時的區別?
在分析上述ThreadLocalMap原始碼的時候,注意到有一個小細節,ThreadLocalMap的Entry繼承了WeakReference<ThreadLocal<?>>,也就是說Entry的key是一個對ThreadLocal<?>的弱參考,問題來了,為什么這里要使用弱參考呢?

使用強參考會如何?
現在假設Entry的key是一個對ThreadLocal的強參考,當ThreadLocal物件使用完后,外部的強參考不存在,但是因為當前執行緒物件中的threadLocals還持有ThreadLocal的強參考,而threadLocals的生命周期是和執行緒一致的,這個時候,如果沒有手動洗掉,整個Entry就發生了記憶體泄露,
使用弱參考會如何?
現在假設Entry的key是一個對ThreadLocal的弱參考,當ThreadLocal物件使用完后,外部的強參考不存在,此時ThreadLocal物件只存在Entry中key對它的弱參考,在下次GC的時候,這個ThreadLocal物件就會被回收,導致key為null,此時value的強參考還存在,但是value已經不會被使用了,如果沒有手動洗掉,那么這個Entry中的key就會發生記憶體泄露,
使用弱參考還有一些好處,那就是,當key為null時, ThreadLocalMap中最多存在一個key為null,并且當呼叫set(),get(),remove()這些方法的時候,是會清除掉key為null的entry的,
set()、get()、remove() 方法中相關實作
- 從下可以發現,set方法首先會進入一個回圈,
- 在這個回圈中,會遍歷整個Entry陣列,直到遇到一個空的entry,退出回圈,
- 當遇到已存在的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;
}
// 發現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發生記憶體泄露的問題,如果不手動清除,那么這個不被使用的entry會一直存在,
- ThreadLocal如果對ThreadLocalMap的key使用弱參考,那么可能會存在一個entry的value發生記憶體泄露,但是在呼叫set(),get(),remove() 方法時,key為null的entry會被清除掉,
- 發生記憶體泄露最根本的原因是:threadLocals的生命周期是和執行緒一致的,
- 每次使用完ThreadLocal物件后,必須呼叫它的remove()方法清除資料,
5.ThreadLocal應用
ThreadLocal把資料存放到執行緒本地,解決了執行緒安全問題,沒有使用鎖,直接訪問執行緒本地變數,效率較高(空間換時間,)
同時threadLocals的生命周期是和執行緒一致的,可以解決很多引數傳遞問題,
- Session管理(Mabaties使用ThreadLocal存盤session),資料庫連接,
- 如果需要跟蹤請求的整個流程,可以使用ThreadLocal來傳遞引數,
ATFWUS 2021-11-09
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/354540.html
標籤:其他
下一篇:Leetcode 7. 整數反轉
