我想ThreadLocal這東西,你們或多或少都瞭解過一點,我在接觸ThreadLocal的時候,以爲這東西很神奇,在網上看了不少博客,也看了一些書,總以爲有一個坎跨不過去,因此對ThreadLocal一直是隻知其一;不知其二的,好在這東西在實際開發中畢竟用的很少,因此也就得過且過了。固然我說的「用的很少」,只是對於普通的上層業務開發而言,其實在不少框架中,都用到了ThreadLocal,甚至有的還對ThreadLocal作了進一步的改進。可是ThreadLocal也算是併發編程的基礎,因此還真的有必要,也必需要好好研究下的。今天咱們就來好好看看ThreadLocal。編程
咱們知道在多線程下,操做一個共享變量,很容易會發生矛盾,要解決這問題,最好的辦法固然是每一個線程都擁有本身的變量,其餘的線程沒法訪問,所謂「沒有共享,就沒有傷害」。那麼如何作到呢?ThreadLocal就這樣華麗麗的登場了。數組
咱們先來看看簡單的應用:bash
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("Hello");
System.out.println("當前線程是:" + Thread.currentThread().getName());
System.out.println("在當前線程中獲取:" + threadLocal.get());
new Thread(() -> System.out.println("如今線程是"+Thread.currentThread().getName()+"嘗試獲取:" + threadLocal.get())).start();
}
複製代碼
運行結果:多線程
當前線程是:main
在當前線程中獲取:Hello
如今線程是Thread-0嘗試獲取:null
複製代碼
運行結果很好理解,在主線程中往threadLocal 塞了一個值,只有在同一個線程下,才能夠得到值,在其餘線程就沒法獲取值了。併發
在咱們探究ThreadLocal以前,先讓咱們思考一個問題,若是叫你來實現ThreadLocal,你會怎麼作? ThreadLocal的目標就在於讓每一個線程都有隻屬於本身的變量。最直接的辦法就是新建一個泛型類,在類中定義一個map,key是Long類型的,用來保存線程的id,value是T類型的,用來保存具體的數據。框架
set的時候,就獲取當前線程的id,把這個做爲key,往map裏面塞數據;工具
get的時候,仍是獲取當前線程的id,把這個做爲key,而後從map中取出數據。ui
就像下面這個樣子:this
public class ThreadLocalTest {
public static void main(String[] args) {
CodeBearThreadLocal threadLocal = new CodeBearThreadLocal();
threadLocal.set("Hello");
System.out.println("當前線程是:" + Thread.currentThread().getName());
System.out.println("在當前線程中獲取:" + threadLocal.get());
new Thread(() -> System.out.println("如今線程是" + Thread.currentThread().getName() + "嘗試獲取:" + threadLocal.get())).start();
}
}
class CodeBearThreadLocal<T> {
private ConcurrentHashMap<Long , T> hashMap = new ConcurrentHashMap<>();
void set(T value) {
hashMap.put(Thread.currentThread().getId(),value);
}
T get() {
return hashMap.get(Thread.currentThread().getId());
}
}
複製代碼
運行結果:spa
當前線程是:main
在當前線程中獲取:Hello
如今線程是Thread-0嘗試獲取:null
複製代碼
能夠看到運行結果和「正版的ThreadLocal」是如出一轍的。
咱們本身也寫了一個ThreadLocal,看上去一點問題也沒有,僅僅幾行代碼就把功能實現了,給本身鼓個掌。那正版的ThreadLocal是怎麼實現的呢?核心應該和咱們寫的差很少吧。遺憾的是,正版的ThreadLocal和咱們寫的能夠說徹底不同。
咱們如今看看正版的ThreadLocal是怎麼作的。
public void set(T value) {
Thread t = Thread.currentThread();//獲取當前的線程
ThreadLocalMap map = getMap(t);//獲取ThreadLocalMap
if (map != null)//若是map不爲null,調用set方法塞入值
map.set(this, value);
else
createMap(t, value);//新建map
}
複製代碼
讓咱們來看看getMap方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
複製代碼
getMap方法比較簡單,直接返回了傳進來的線程對象的threadLocals,說明threadLocals定義在Thread類裏面,是ThreadLocalMap 類型的,讓咱們看看threadLocals的定義:
public class Thread implements Runnable{
ThreadLocal.ThreadLocalMap threadLocals = null;
}
複製代碼
看到這個定義,你們必定有點暈,咱們是跟着ThreadLocal的set方法進來的,怎麼到了這裏又回到ThreadLocal了,你們彆着急,咱們再來看看ThreadLocalMap是什麼鬼?
讓咱們把關係理一理,確實有點混亂,Thread類裏面定義了ThreadLocal.ThreadLocalMap字段,ThreadLocalMap是TheadLocal的內部靜態類,其中的Entry[]是用來保存數據的。這就意味着,每個Thread實例中的ThreadLocalMap都是獨一無二的,又不相互干擾。等等,這不就揭開了ThreadLocal的神祕面紗了嗎?原來ThreadLocal是這麼作到讓每一個線程都有本身的變量的。
若是你還不清楚的話,不要緊,咱們再來講的詳細點。在咱們實現的ThreadLocal中,是利用map實現數據存儲的,key就是線程Id,你能夠理解爲key就是Thread的實例,value就是咱們須要保存的數據,當咱們調用get方法的時候,就是利用線程Id,你能夠理解爲利用Thread的實例去map中取出數據,這樣咱們取出的數據就確定是這個線程持有的。好比這個線程是A,你傳入了B線程的線程Id,也就是傳入了B線程的Thread的實例就確定沒法取出線程A所持有的數據,這點應該毫無疑問把。可是,在正版的ThreadLocal中,數據是直接存在Thread實例中的,這樣每一個線程的數據就被自然的隔離了。
如今咱們解決了一個問題,ThreadLocal是如何實現線程數據隔離的,可是還有一個問題,也就是我初學ThreadLocal看了不少博客,仍然百思不得其解的問題,既然數據是保存在ThreadLocalMap中的Entry[]的,那麼就表明能夠保存多個數據,否則用一個普通的成員變量不就OK了嗎,爲何要用數組呢?可是ThreadLocal提供的set方法沒有重載啊,若是先set一個「hello」,又set一個「bye」,那麼「bye」確定會把「hello」給覆蓋掉啊,又不像HashMap同樣,有key和value的概念。這個問題真的困擾我好久,後面終於知道了緣由了,咱們能夠new多個ThreadLocal呀,就像這樣:
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
threadLocal1.set("Hello");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("Bye");
}
複製代碼
這樣一來,會發生什麼狀況呢?再次放出set的代碼,以避免你們要往上翻好久:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
複製代碼
threadLocal1,threadLocal2都調用了set方法,儘管threadLocal1和threadLocal2是不一樣的實例,可是它們在同一個線程啊,因此getMap獲取的ThreadLocalMap是同一個,這樣就變成了在同一個ThreadLocalMap保存了多個數據。
具體是怎麼保存數據的,這個代碼就比較複雜了,包括的細節太多了,我看的也不是很懂,只知道一個大概,咱們先來看看Entry的定義把:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
複製代碼
Entry又是ThreadLocalMap的靜態內部類,裏面只有一個字段value,也就是說和HashMap是不一樣的,沒有鏈表的概念。
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();
}
複製代碼
其中的細節有點多,看的有點迷糊,可是最關鍵的應該還算是看懂了。
public T get() {
Thread t = Thread.currentThread();//獲取當前線程
ThreadLocalMap map = getMap(t);//傳入當前線程,獲取當前線程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//傳入ThreadLocal實例,獲取Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;//返回值
}
}
return setInitialValue();
}
複製代碼
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);
}
複製代碼
set方法和get方法都分析完畢了,咱們來作一個小總結。咱們在外面所使用的ThreadLocal更像是一個工具類,自己不保存任何數據,而真正的數據是保存在Thread實例中的,這樣就自然的完成了線程數據的隔離。最後送上一張圖,來幫助你們更好的理解ThreadLocal:
咱們再來看看Entry的定義:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
複製代碼
Entry繼承了WeakReference,關於WeakReference是什麼東西,不是本文的重點,你們能夠自行查閱。WeakReference包裹了ThreadLocal,咱們再來看Entry的構造方法,調用了super(k),傳入了咱們傳進來的ThreadLocal實例,也就是ThreadLocal被保存到了WeakReference對象中。這就致使了一個問題,當ThreadLocal沒有強依賴,ThreadLocal會在下一次發生GC時被回收,key是被回收了,可是value卻沒有被回收呀,因此就出現了Entry[]存在key爲NULL,可是value不爲NULL的項的狀況,要想回收的話,可讓建立ThreadLocal的線程的生命週期結束。可是在實際的開發中,線程有極大多是和程序同生共死的,只要程序不中止,線程就一直在蹦躂。因此咱們在使用完ThreadLocal方法後,最好要手動調用remove方法,就像這樣:
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal();
try {
threadLocal.set("Hello");
threadLocal.get();
} finally {
threadLocal.remove();
}
}
複製代碼
別忘了,最好把remove方法放在finally中哦。
咱們仍是來看博客一開頭的例子:
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("Hello");
System.out.println("當前線程是:" + Thread.currentThread().getName());
System.out.println("在當前線程中獲取:" + threadLocal.get());
new Thread(() -> System.out.println("如今線程是" + Thread.currentThread().getName() + "嘗試獲取:" + threadLocal.get())).start();
}
複製代碼
運行結果:
當前線程是:main
在當前線程中獲取:Hello
如今線程是Thread-0嘗試獲取:null
複製代碼
代碼後面new出來Thread是由主線程建立的,因此能夠說這個線程是主線程的子線程,在主線程往ThreadLocal set的值,在子線程中獲取不到,這很好理解,由於他們並非同一個線程,可是我但願子線程能繼承主線程的ThreadLocal中的數據。InheritableThreadLocal出現了,徹底能夠知足這樣的需求:
public static void main(String[] args) {
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("Hello");
System.out.println("當前線程是:" + Thread.currentThread().getName());
System.out.println("在當前線程中獲取:" + threadLocal.get());
new Thread(() -> System.out.println("如今線程是" + Thread.currentThread().getName() + "嘗試獲取:" + threadLocal.get())).start();
}
複製代碼
運行結果:
當前線程是:main
在當前線程中獲取:Hello
如今線程是Thread-0嘗試獲取:Hello
複製代碼
這樣就讓子線程繼承了主線程的ThreadLocal的數據,說的更準確些,是子線程繼承了父線程的ThreadLocal的數據。
那究竟是如何作到的呢?仍是看代碼把。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
複製代碼
InheritableThreadLocal繼承了ThreadLocal,而且重寫了三個方法,當咱們首次調用InheritableThreadLocal的set的時候,會調用InheritableThreadLocal的createMap方法,這就建立了ThreadLocalMap的實例,而且賦值給inheritableThreadLocals,這個inheritableThreadLocals定義在哪裏呢?和ThreadLocal的threadLocals同樣,也是定義在Thread類中。當咱們再次調用set方法的時候,會調用InheritableThreadLocal的getMap方法,返回的也是inheritableThreadLocals,也就是把原先的threadLocals給替換掉了。
當咱們建立一個線程,會調用Thread的構造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
複製代碼
init方法比較長,我只複製出和咱們要探究的問題相關的代碼:
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
複製代碼
這篇博客到這裏就結束了,東西仍是挺多的,可是都是挺重要的,特別是ThreadLocal的緣由和產生內存泄露的緣由和避免的方法。