1、ThreadLocal簡介
ThreadLocal是一個以ThreadLocal對象為鍵、任意對象為值的存儲結構,提供了線程本地變量,也就是如果建立了一個ThreadLocal變量,那麼通路這個變量的每個線程都會有這個變量的一個本地副本。當多個線程操作這個變量時,實際操作的是自己本地記憶體裡面的變量,進而避免了線程安全問題。建立一個ThreadLocal變量後,每個線程都會複制一個變量到自己的本地記憶體
ThreadLocal的内部結構圖如下:
2、ThreadLocal使用示例
public class ThreadLocalTest {
static void print(String str) {
System.out.println(str + ":" + localVariable.get());
localVariable.remove();
}
static ThreadLocal<String> localVariable = new ThreadLocal<String>();
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
localVariable.set("threadOne local variable");
print("threadOne");
System.out.println("threadOne remove after :" + localVariable.get());
}
}).start();
new Thread(new Runnable() {
public void run() {
localVariable.set("threadTwo local variable");
print("threadTwo");
System.out.println("threadTwo remove after :" + localVariable.get());
}
}).start();
}
}
運作結果:
threadOne:threadOne local variable
threadOne remove after :null
threadTwo:threadTwo local variable
threadTwo remove after :null
3、ThreadLocal的實作原理
Thread類中有一個threadLocals和一個inheritableThreadLocals,它們都是ThreadLocalMap類型的變量,而ThreadLocalMap是一個定制化的HashMap。在預設情況下,每個線程中的這兩個變量都為null,隻有目前線程第一次調用ThreadLocal的set()或者get()方法時才會建立它們。其實每個線程的本地變量不是存放在ThreadLocal執行個體裡面,而是存放在調用線程的threadLocals變量裡面。也就是說,ThreadLocal類型的本地變量存放在具體的線程記憶體空間中。ThreadLocal就是一個工具殼,它通過set()方法把value值放入調用線程的threadLocals裡面并存放起來,當調用線程調用它的get()方法時,再從目前線程的threadLocals變量裡面将其拿出來使用。如果調用線程一直不終止,那麼這個本地變量會一直存放在調用線程的threadLocals變量裡面,是以當不需要使用本地變量時可以通過調用ThreadLocal變量的remove()方法,從目前線程的threadLocals裡面删除該本地變量
1)、void set(T value)
public void set(T value) {
//擷取目前線程
Thread t = Thread.currentThread();
//找到目前線程對應的threadLocals變量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//第一次調用就建立目前線程對應的threadLocals變量
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
//擷取線程自己的變量threadLocals,threadLocals變量被綁定到了線程的成員變量上
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//建立目前線程的threadLocals變量
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
2)、T get()
public T get() {
//擷取目前線程
Thread t = Thread.currentThread();
//擷取目前線程的threadLocals變量
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果threadLocals不為null,則傳回對應本地變量的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//threadLocals為空則初始化目前線程的threadLocals成員變量
return setInitialValue();
}
private T setInitialValue() {
//初始化為null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果目前線程的threadLocals變量不為null
if (map != null)
map.set(this, value);
//如果目前線程的threadLocals變量為null
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
3)、void remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
如果目前線程的threadLocals變量不為空,則删除目前線程中指定ThreadLocal執行個體的本地變量
4)、ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部類,沒有實作Map接口,用獨立的方式實作了Map的功能,其内部的Entry也獨立實作的
在ThreadLocalMap中,也是用Entry來儲存K-V結構資料的,但是Entry中key隻能是ThreadLocal對象
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry繼承自WeakReference(弱引用,生命周期隻能存活到下次GC前),但隻有key是弱引用類型的,value并非弱引用
private static final int INITIAL_CAPACITY = 16;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
從ThreadLocalMap的構造函數可以得知,ThreadLocalMap初始化容量為16,負載因子為2/3
和HashMap的最大的不同在于,ThreadLocalMap結構非常簡單,沒有next引用,也就是說ThreadLocalMap中解決Hash沖突的方式并非連結清單的方式,而是采用線性探測的方式。所謂線性探測,就是根據初始key的hashcode值确定元素在table數組中的位置,如果發現這個位置上已經有其他key值的元素被占用,則利用固定的算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置
private void set(ThreadLocal<?> key, Object value) {
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;
}
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);
}
在插入過程中,根據ThreadLocal對象的hash值,定位到table中的位置i,過程如下
- 如果目前位置是空的,那麼正好,就初始化一個Entry對象放在位置i上
- 位置i已有對象,如果這個Entry對象的key正好是即将設定的key,那麼覆寫value
- 位置i的對象,和即将設定的key沒關系,那麼隻能找下一個空位置
5)、ThreadLocalMap記憶體洩露問題
上圖中,實線代表強引用,虛線代表的是弱引用,如果threadLocal外部強引用被置為null(
threadLocalInstance==null
)的話,threadLocal執行個體就沒有一條引用鍊路可達,很顯然在GC(垃圾回收)的時候勢必會被回收,是以entry就存在key為null的情況,無法通過一個Key為null去通路到該entry的value。同時,就存在了這樣一條引用鍊:
threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory
,導緻在垃圾回收的時候進行可達性分析的時候,value可達進而不會被回收掉,但是該value永遠不能被通路到,這樣就存在了記憶體洩漏。當然,如果線程執行結束後,threadLocal和threadRef會斷掉,是以threadLocal、threadLocalMap、entry都會被回收掉。可是,在實際使用中我們都是會用線程池去維護我們的線程,比如在
Executors.newFixedThreadPool()
時建立線程的時候,為了複用線程是不會結束的,是以threadLocal記憶體洩漏就值得我們關注
ThreadLocalMap的設計中已經做出了哪些改進?
ThreadLocalMap中的get()和set()方法都針對記憶體洩露問題做了相應的處理,下文為了叙述,針對key為null的entry,源碼注釋為stale entry,就稱之為“髒entry”
ThreadLocalMap的set()方法:
private void set(ThreadLocal<?> key, Object value) {
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;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
在該方法中針對髒entry做了這樣的處理:
- 如果目前table[i]不為空的話說明hash沖突就需要向後環形查找,若在查找過程中遇到髒entry就通過replaceStaleEntry()進行處理
- 如果目前table[i]為空的話說明新的entry可以直接插入,但是插入後會調用cleanSomeSlots()方法檢測并清除髒entry
當我們調用threadLocal的get()方法時,當table[i]不是和所要找的key相同的話,會繼續通過threadLocalMap的getEntryAfterMiss()方法向後環形去找
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;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
當key為null的時候,即遇到髒entry也會調用expungeStleEntry()對髒entry進行清理
當我們調用threadLocal.remove()方法時候,實際上會調用threadLocalMap的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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
同樣的可以看出,當遇到了key為null的髒entry的時候,也會調用expungeStaleEntry()清理掉髒entry
從以上set()、get()、remove()方法看出,在ThreadLocal的生命周期裡,針對ThreadLocal存在的記憶體洩漏的問題,都會通過expungeStaleEntry()、cleanSomeSlots()、replaceStaleEntry()這三個方法清理掉key為null的髒entry
想要更加深入學習ThreadLocal記憶體洩漏問題可以檢視這篇文章
小結:
在每個線程内部都有一個名為threadLocals的成員變量,該變量的類型為HashMap,其中key為我們定義的ThreadLocal變量的this引用,value則為我們使用set方法設定的值。每個線程的本地變量存放線上程自己的記憶體變量threadLocals中,如果目前線程一直不消亡,那麼這些本地變量會一直存在,是以可能會造成記憶體溢岀,因 此使用完畢後要記得調用ThreadLocal的remove()方法删除對應線程的threadLocals中的本地變量
4、InheritableThreadLocal類
同一個ThreadLocal變量在父線程中被設定值後,在子線程中是擷取不到的。而子類InheritableThreadLocal提供了一個特性,就是讓子線程可以通路在父線程中設定的本地變量
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
//(1)
protected T childValue(T parentValue) {
return parentValue;
}
//(2)
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
//(3)
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
從上面InheritableThreadLocal的源碼中可知,InheritableThreadLocal繼承了ThreadLocal,并重寫了三個方法。由代碼(3)可知,InheritableThreadLocal重寫了createMap()方法,那麼現在當第一次調用set()方法時,建立的是目前線程的inheritableThreadLocals變量的執行個體而不再是threadLocals。由代碼(2)可知,當調用get()方法擷取目前線程内部的map變量時,擷取的是inheritableThreadLocals而不再是threadLocals
綜上可知,在InheritableThreadLocal的世界裡,變量inheritableThreadLocals替代了threadLocals
下面我們看一下重寫的代碼(1)何時執行,以及如何讓子線程可以通路父線程的本地變量。這要從建立Thread的代碼說起,打開Thread類的預設構造函數
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
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) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
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);
//如果父線程的inheritableThreadLocals變量不為null
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//設定子線程中的inheritableThreadLocals變量
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID();
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
在createlnheritedMap内部使用父線程的inheritableThreadLocals變量作為構造函數建立了一個新的ThreadLocalMap變量,然後指派給了子線程的inheritableThreadLocals變量
小結:
InheritableThreadLocal類通過重寫代碼(2)和(3)讓本地變量儲存到了具體線程的inheritableThreadLocals變量裡面,那麼線程在通過InheritableThreadLocal類執行個體的set()或者get()方法設定變量時,就會建立目前線程的inheritableThreadLocals變量。當父線程建立子線程時,構造函數會把父線程中inheritableThreadLocals變量裡面的本地變量複制一份儲存到子線程的inheritableThreadLocals變量裡面
參考:
https://www.jianshu.com/p/98b68c97df9b
https://blog.csdn.net/wsm0712syb/article/details/51025111
https://www.jianshu.com/p/dde92ec37bd1