原創文章,始自發做者我的博客,轉載請務必將下面這段話置於文章開頭處(保留超連接)。
本文轉發自技術世界,原文連接 http://www.jasongj.com/java/threadlocal/java
因爲 ThreadLocal 支持範型,如 ThreadLocal< StringBuilder >,爲表述方便,後文用 變量 表明 ThreadLocal 自己,而用 實例 表明具體類型(如 StringBuidler )的實例。react
寫這篇文章的一個緣由在於,網上不少博客關於 ThreadLocal 的適用場景以及解決的問題,描述的並不清楚,甚至是錯的。下面是常見的對於 ThreadLocal的介紹安全
ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路
ThreadLocal的目的是爲了解決多線程訪問資源時的共享問題session
還有不少文章在對比 ThreadLocal 與 synchronize 的異同。既然是做比較,那應該是認爲這二者解決相同或相似的問題。多線程
上面的描述,問題在於,ThreadLocal 並不解決多線程 共享 變量的問題。既然變量不共享,那就更談不上同步的問題。併發
ThreadLoal 變量,它的基本原理是,同一個 ThreadLocal 所包含的對象(對ThreadLocal< String >而言即爲 String 類型變量),在不一樣的 Thread 中有不一樣的副本(實際是不一樣的實例,後文會詳細闡述)。這裏有幾點須要注意app
那 ThreadLocal 到底解決了什麼問題,又適用於什麼樣的場景?less
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).ide
核心意思是ui
ThreadLocal 提供了線程本地的實例。它與普通變量的區別在於,每一個使用該變量的線程都會初始化一個徹底獨立的實例副本。ThreadLocal 變量一般被
private static
修飾。當一個線程結束時,它所使用的全部 ThreadLocal 相對的實例副本均可被回收。
總的來講,ThreadLocal 適用於每一個線程須要本身獨立的實例且該實例須要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景。後文會經過實例詳細闡述該觀點。另外,該場景下,並不是必須使用 ThreadLocal ,其它方式徹底能夠實現一樣的效果,只是 ThreadLocal 使得實現更簡潔。
下面經過以下代碼說明 ThreadLocal 的使用方式
public class ThreadLocalDemo { public static void main(String[] args) throws InterruptedException { int threads = 3; CountDownLatch countDownLatch = new CountDownLatch(threads); InnerClass innerClass = new InnerClass(); for(int i = 1; i <= threads; i++) { new Thread(() -> { for(int j = 0; j < 4; j++) { innerClass.add(String.valueOf(j)); innerClass.print(); } innerClass.set("hello world"); countDownLatch.countDown(); }, "thread - " + i).start(); } countDownLatch.await(); } private static class InnerClass { public void add(String newStr) { StringBuilder str = Counter.counter.get(); Counter.counter.set(str.append(newStr)); } public void print() { System.out.printf("Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n", Thread.currentThread().getName(), Counter.counter.hashCode(), Counter.counter.get().hashCode(), Counter.counter.get().toString()); } public void set(String words) { Counter.counter.set(new StringBuilder(words)); System.out.printf("Set, Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n", Thread.currentThread().getName(), Counter.counter.hashCode(), Counter.counter.get().hashCode(), Counter.counter.get().toString()); } } private static class Counter { private static ThreadLocal<StringBuilder> counter = new ThreadLocal<StringBuilder>() { @Override protected StringBuilder initialValue() { return new StringBuilder(); } }; } }
ThreadLocal自己支持範型。該例使用了 StringBuilder 類型的 ThreadLocal 變量。可經過 ThreadLocal 的 get() 方法讀取 StringBuidler 實例,也可經過 set(T t) 方法設置 StringBuilder。
上述代碼執行結果以下
Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:0 Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:0 Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:0 Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:01 Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:01 Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:012 Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:0123 Set, Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1362597339, Value:hello world Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:01 Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:012 Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:012 Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:0123 Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:0123 Set, Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:482932940, Value:hello world Set, Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1691922941, Value:hello world
從上面的輸出可看出
既然每一個訪問 ThreadLocal 變量的線程都有本身的一個「本地」實例副本。一個可能的方案是 ThreadLocal 維護一個 Map,鍵是 Thread,值是它在該 Thread 內的實例。線程經過該 ThreadLocal 的 get() 方案獲取實例時,只須要以線程爲鍵,從 Map 中找出對應的實例便可。該方案以下圖所示
該方案可知足上文提到的每一個線程內一個獨立備份的要求。每一個新線程訪問該 ThreadLocal 時,須要向 Map 中添加一個映射,而每一個線程結束時,應該清除該映射。這裏就有兩個問題:
其中鎖的問題,是 JDK 未採用該方案的一個緣由。
上述方案中,出現鎖的問題,緣由在於多線程訪問同一個 Map。若是該 Map 由 Thread 維護,從而使得每一個 Thread 只訪問本身的 Map,那就不存在多線程寫的問題,也就不須要鎖。該方案以下圖所示。
該方案雖然沒有鎖的問題,可是因爲每一個線程訪問某 ThreadLocal 變量後,都會在本身的 Map 內維護該 ThreadLocal 變量與具體實例的映射,若是不刪除這些引用(映射),則這些 ThreadLocal 不能被回收,可能會形成內存泄漏。後文會介紹 JDK 如何解決該問題。
該方案中,Map 由 ThreadLocal 類的靜態內部類 ThreadLocalMap 提供。該類的實例維護某個 ThreadLocal 與具體實例的映射。與 HashMap 不一樣的是,ThreadLocalMap 的每一個 Entry 都是一個對 鍵 的弱引用,這一點從super(k)
可看出。另外,每一個 Entry 都包含了一個對 值 的強引用。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
使用弱引用的緣由在於,當沒有強引用指向 ThreadLocal 變量時,它可被回收,從而避免上文所述 ThreadLocal 不能被回收而形成的內存泄漏的問題。
可是,這裏又可能出現另一種內存泄漏的問題。ThreadLocalMap 維護 ThreadLocal 變量與具體實例的映射,當 ThreadLocal 變量被回收後,該映射的鍵變爲 null,該 Entry 沒法被移除。從而使得實例被該 Entry 引用而沒法被回收形成內存泄漏。
注:Entry雖然是弱引用,但它是 ThreadLocal 類型的弱引用(也即上文所述它是對 鍵 的弱引用),而非具體實例的的弱引用,因此沒法避免具體實例相關的內存泄漏。
讀取實例方法以下所示
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(); }
讀取實例時,線程首先經過getMap(t)
方法獲取自身的 ThreadLocalMap。從以下該方法的定義可見,該 ThreadLocalMap 的實例是 Thread 類的一個字段,即由 Thread 維護 ThreadLocal 對象與具體實例的映射,這一點與上文分析一致。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
獲取到 ThreadLocalMap 後,經過map.getEntry(this)
方法獲取該 ThreadLocal 在當前線程的 ThreadLocalMap 中對應的 Entry。該方法中的 this 即當前訪問的 ThreadLocal 對象。
若是獲取到的 Entry 不爲 null,從 Entry 中取出值即爲所需訪問的本線程對應的實例。若是獲取到的 Entry 爲 null,則經過setInitialValue()
方法設置該 ThreadLocal 變量在該線程中對應的具體實例的初始值。
設置初始值方法以下
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; }
該方法爲 private 方法,沒法被重載。
首先,經過initialValue()
方法獲取初始值。該方法爲 public 方法,且默認返回 null。因此典型用法中經常重載該方法。上例中即在內部匿名類中將其重載。
而後拿到該線程對應的 ThreadLocalMap 對象,若該對象不爲 null,則直接將該 ThreadLocal 對象與對應實例初始值的映射添加進該線程的 ThreadLocalMap中。若爲 null,則先建立該 ThreadLocalMap 對象再將映射添加其中。
這裏並不須要考慮 ThreadLocalMap 的線程安全問題。由於每一個線程有且只有一個 ThreadLocalMap 對象,而且只有該線程本身能夠訪問它,其它線程不會訪問該 ThreadLocalMap,也即該對象不會在多個線程中共享,也就不存在線程安全的問題。
除了經過initialValue()
方法設置實例的初始值,還可經過 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); }
該方法先獲取該線程的 ThreadLocalMap 對象,而後直接將 ThreadLocal 對象(即代碼中的 this)與目標實例的映射添加進 ThreadLocalMap 中。固然,若是映射已經存在,就直接覆蓋。另外,若是獲取到的 ThreadLocalMap 爲 null,則先建立該 ThreadLocalMap 對象。
對於已經再也不被使用且已被回收的 ThreadLocal 對象,它在每一個線程內對應的實例因爲被線程的 ThreadLocalMap 的 Entry 強引用,沒法被回收,可能會形成內存泄漏。
針對該問題,ThreadLocalMap 的 set 方法中,經過 replaceStaleEntry 方法將全部鍵爲 null 的 Entry 的值設置爲 null,從而使得該值可被回收。另外,會在 rehash 方法中經過 expungeStaleEntry 方法將鍵和值爲 null 的 Entry 設置爲 null 從而使得該 Entry 可被回收。經過這種方式,ThreadLocal 可防止內存泄漏。
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(); }
如上文所述,ThreadLocal 適用於以下兩種場景
對於第一點,每一個線程擁有本身實例,實現它的方式不少。例如能夠在線程內部構建一個單獨的實例。ThreadLoca 能夠以很是方便的形式知足該需求。
對於第二點,能夠在知足第一點(每一個線程有本身的實例)的條件下,經過方法間引用傳遞的形式實現。ThreadLocal 使得代碼耦合度更低,且實現更優雅。
對於 Java Web 應用而言,Session 保存了不少信息。不少時候須要經過 Session 獲取信息,有些時候又須要修改 Session 的信息。一方面,須要保證每一個線程有本身單獨的 Session 實例。另外一方面,因爲不少地方都須要操做 Session,存在多方法共享 Session 的需求。若是不使用 ThreadLocal,能夠在每一個線程內構建一個 Session實例,並將該實例在多個方法間傳遞,以下所示。
public class SessionHandler { @Data public static class Session { private String id; private String user; private String status; } public Session createSession() { return new Session(); } public String getUser(Session session) { return session.getUser(); } public String getStatus(Session session) { return session.getStatus(); } public void setStatus(Session session, String status) { session.setStatus(status); } public static void main(String[] args) { new Thread(() -> { SessionHandler handler = new SessionHandler(); Session session = handler.createSession(); handler.getStatus(session); handler.getUser(session); handler.setStatus(session, "close"); handler.getStatus(session); }).start(); } }
該方法是能夠實現需求的。可是每一個須要使用 Session 的地方,都須要顯式傳遞 Session 對象,方法間耦合度較高。
這裏使用 ThreadLocal 從新實現該功能以下所示。
public class SessionHandler { public static ThreadLocal<Session> session = new ThreadLocal<Session>(); @Data public static class Session { private String id; private String user; private String status; } public void createSession() { session.set(new Session()); } public String getUser() { return session.get().getUser(); } public String getStatus() { return session.get().getStatus(); } public void setStatus(String status) { session.get().setStatus(status); } public static void main(String[] args) { new Thread(() -> { SessionHandler handler = new SessionHandler(); handler.getStatus(); handler.getUser(); handler.setStatus("close"); handler.getStatus(); }).start(); } }
使用 ThreadLocal 改造後的代碼,再也不須要在各個方法間傳遞 Session 對象,而且也很是輕鬆的保證了每一個線程擁有本身獨立的實例。
若是單看其中某一點,替代方法不少。好比可經過在線程內建立局部變量可實現每一個線程有本身的實例,使用靜態變量可實現變量在方法間的共享。但若是要同時知足變量在線程間的隔離與方法間的共享,ThreadLocal再合適不過。