1、簡介
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。使用這個工具類能夠很簡潔地編寫出優美的多線程程序。當使用ThreadLocal維護變量時,ThreadLocal爲每一個使用該變量的線程提供獨立的變量副本,因此每個線程均可以獨立地改變本身的副本,而不會影響其它線程所對應的副本。從線程的角度看,目標變量就象是線程的本地變量,這也是類名中「Local」所要表達的意思。因此,在Java中編寫線程局部變量的代碼相對來講要笨拙一些,所以形成線程局部變量沒有在Java開發者中獲得很好的普及。人們常說,鎖是一種以時間換空間的機制,而ThreadLocal正好是以空間換時間的。java
變量值的共享可使用public static的形式,全部線程都使用同一個變量,若是想實現每個線程都有本身的共享變量該如何實現呢?JDK中的ThreadLocal類正是爲了解決這樣的問題。mysql
ThreadLocal類並非用來解決多線程環境下的共享變量問題,而是用來提供線程內部的共享變量,在多線程環境下,能夠保證各個線程之間的變量互相隔離、相互獨立。在線程中,能夠經過get()/set()方法來訪問變量。ThreadLocal實例一般來講都是private static類型的,它們但願將狀態與線程進行關聯。這種變量在線程的生命週期內起做用,能夠減小同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度。sql
2、和鎖的比較
兩個概念:線程安全,線程同步安全
事實上,ThreadLocal只解決線程安全的問題,並不能解決線程同步的問題,ThreadLocal既然爲每一個線程拷貝一份變量,不必再進行同步,ThreadLocal並非用來解決線程同步的,因此它與鎖能夠說是沒有什麼關係的。多線程
總結:ThreadLocal解決的是同一個線程內的資源共享問題,而synchronized 解決的是多個線程間的資源共享問題。併發
3、ThreadLocal
ThreadLocal類接口很簡單,只有4個方法:
void set(T value)設置當前線程的線程局部變量的值。
public T get()該方法返回當前線程所對應的線程局部變量。
public void remove()將當前線程局部變量的值刪除,目的是爲了減小內存的佔用,該方法是JDK 5.0新增的方法。須要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,因此顯式調用該方法清除線程的局部變量並非必須的操做,但它能夠加快內存回收的速度。
protected T initialValue()返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(T value)時才執行,而且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。ide
在JDK5.0中,ThreadLocal已經支持泛型,該類的類名已經變爲ThreadLocal<T>。API方法也相應進行了調整,新版本的API方法分別是void set(T value)、T get()以及T initialValue()。ThreadLocal是如何作到爲每個線程維護變量的副本的呢?其實實現的思路很簡單:在ThreadLocal類中有一個Map,用於存儲每個線程的變量副本,Map中元素的鍵爲線程對象,而值對應線程的變量副本。本身就能夠提供一個簡單的實現版本:函數
package com.test; public class TestNum { // ①經過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值 private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() { public Integer initialValue() { return 0; } }; // ②獲取下一個序列值 public int getNextNum() { seqNum.set(seqNum.get() + 1); return seqNum.get(); } public static void main(String[] args) { TestNum sn = new TestNum(); // ③ 3個線程共享sn,各自產生序列號 TestClient t1 = new TestClient(sn); TestClient t2 = new TestClient(sn); TestClient t3 = new TestClient(sn); t1.start(); t2.start(); t3.start(); } private static class TestClient extends Thread { private TestNum sn; public TestClient(TestNum sn) { this.sn = sn; } public void run() { for (int i = 0; i < 3; i++) { // ④每一個線程打出3個序列值 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" + sn.getNextNum() + "]"); } } } }
一般咱們經過匿名內部類的方式定義ThreadLocal的子類,提供初始的變量值,如例子中①處所示。TestClient線程產生一組序列號,在③處,咱們生成3個TestClient,它們共享同一個TestNum實例。運行以上代碼,在控制檯上輸出如下的結果: 工具
thread[Thread-0] --> sn[1] thread[Thread-1] --> sn[1] thread[Thread-2] --> sn[1] thread[Thread-1] --> sn[2] thread[Thread-0] --> sn[2] thread[Thread-1] --> sn[3] thread[Thread-2] --> sn[2] thread[Thread-0] --> sn[3] thread[Thread-2] --> sn[3]
Thread同步機制的比較
package com.test; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; public class TestDao { private Connection conn;// ①一個非線程安全的變量 public void addTopic() throws SQLException { Statement stat = conn.createStatement();// ②引用非線程安全變量 // … } }
main方法源碼分析
MyThread9 mt = new MyThread9(); Thread threadA = new Thread(mt, "票販子A"); Thread threadB = new Thread(mt, "票販子B"); Thread threadC = new Thread(mt, "票販子C"); threadA.start(); threadB.start(); threadC.start();
輸出
票販子A,ticket=5
票販子C,ticket=4
票販子B,ticket=3
票販子C,ticket=2
票販子C,ticket=1
因爲①處的conn是成員變量,由於addTopic()方法是非線程安全的,必須在使用時建立一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非線程安全的「狀態」進行改造:代碼清單4 TestDao:線程安全
package com.test; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; public class TestDaoNew { // ①使用ThreadLocal保存Connection變量 private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>(); public static Connection getConnection() { // ②若是connThreadLocal沒有本線程對應的Connection建立一個新的Connection, // 並將其保存到線程本地變量中。 if (connThreadLocal.get() == null) { Connection conn = getConnection(); connThreadLocal.set(conn); return conn; } else { return connThreadLocal.get();// ③直接返回線程本地變量 } } public void addTopic() throws SQLException { // ④從ThreadLocal中獲取線程對應的Connection Statement stat = getConnection().createStatement(); } }
不一樣的線程在使用TopicDao時,先判斷connThreadLocal.get()是不是null,若是是null,則說明當前線程尚未對應的Connection對象,這時建立一個Connection對象並添加到本地線程變量中;若是不爲null,則說明當前的線程已經擁有了Connection對象,直接使用就能夠了。這樣,就保證了不一樣的線程使用線程相關的Connection,而不會使用其它線程的Connection。所以,這個TopicDao就能夠作到singleton共享了。固然,這個例子自己很粗糙,將Connection的ThreadLocal直接放在DAO只能作到本DAO的多個方法共享Connection時不發生線程安全問題,但沒法和其它DAO共用同一個Connection,要作到同一事務多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。ConnectionManager.java
package com.test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionManager { private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { @Override protected Connection initialValue() { Connection conn = null; try { conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "username", "password"); } catch (SQLException e) { e.printStackTrace(); } return conn; } }; public static Connection getConnection() { return connectionHolder.get(); } public static void setConnection(Connection conn) { connectionHolder.set(conn); } }
ThreadLocal<T>
ThreadLocal最簡單的實現方式就是ThreadLocal類內部有一個線程安全的Map,而後用線程的ID做爲Map的key,實例對象做爲Map的value,這樣就能達到各個線程的值隔離的效果。
JDK最先期的ThreadLocal就是這樣設計的,可是,以後ThreadLocal的設計換了一種方式。
那麼到底ThreadLocal類是如何實現這種「爲每一個線程提供不一樣的變量拷貝」的呢?先來看一下ThreadLocal的set()方法的源碼是如何實現的:
/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
在這個方法內部咱們看到,首先經過getMap(Thread t)方法獲取一個和當前線程相關的ThreadLocalMap,而後將變量的值設置到這個ThreadLocalMap對象中,固然若是獲取到的ThreadLocalMap對象爲空,就經過createMap方法建立。線程隔離的祕密,就在於ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設置和獲取(對比Map對象來理解),每一個線程中都有一個獨立的ThreadLocalMap副本,它所存儲的值,只能被當前線程讀取和修改。ThreadLocal類經過操做每個線程特有的ThreadLocalMap副本,從而實現了變量訪問在不一樣線程中的隔離。由於每一個線程的變量都是本身特有的,徹底不會有併發錯誤。還有一點就是,ThreadLocalMap存儲的鍵值對中的鍵是this對象指向的ThreadLocal對象,而值就是你所設置的對象了。爲了加深理解,咱們接着看上面代碼中出現的getMap和createMap方法的實現:
/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; } /** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the map * @param map the map to store. */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
接下來再看一下ThreadLocal類中的get()方法
/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); }
get()方法主要作了如下事情:
一、調用Thread.currentThread()獲取當前線程對象t;
二、根據當前線程對象,調用getMap(Thread)獲取線程對應的ThreadLocalMap對象:
如上述
threadLocals是Thread類的成員變量,初始化爲null:
ThreadLocal.ThreadLocalMap threadLocals = null;
再來看setInitialValue()方法:
/** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ 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; }
ThreadLocal的實現離不開ThreadLocalMap類,ThreadLocalMap類是ThreadLocal的靜態內部類。每一個Thread維護一個ThreadLocalMap映射表,這個映射表的key是ThreadLocal實例自己,value是真正須要存儲的Object。這樣的設計主要有如下幾點優點:
這樣設計以後每一個Map的Entry數量變小了:以前是Thread的數量,如今是ThreadLocal的數量,能提升性能;
當Thread銷燬以後對應的ThreadLocalMap也就隨之銷燬了,能減小內存使用量。
ThreadLocalMap源碼分析
ThreadLocalMap是用來存儲與線程關聯的value的哈希表,它具備HashMap的部分特性,好比容量、擴容閾值等,它內部經過Entry類來存儲key和value,Entry類的定義爲:
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,經過上述源碼super(k);能夠知道,ThreadLocalMap是使用ThreadLocal的弱引用做爲Key的。
分析到這裏,咱們能夠獲得下面這個對象之間的引用結構圖(其中,實線爲強引用,虛線爲弱引用):
咱們知道,弱引用對象在Java虛擬機進行垃圾回收時,就會被釋放,那咱們考慮這樣一個問題:
ThreadLocalMap使用ThreadLocal的弱引用做爲key,若是一個ThreadLocal沒有外部關聯的強引用,那麼在虛擬機進行垃圾回收時,這個ThreadLocal會被回收,這樣,ThreadLocalMap中就會出現key爲null的Entry,這些key對應的value也就再無妨訪問,可是value卻存在一條從Current Thread過來的強引用鏈。所以只有當Current Thread銷燬時,value才能獲得釋放。
該強引用鏈以下:
CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value
所以,只要這個線程對象被gc回收,那些key爲null對應的value也會被回收,這樣也沒什麼問題,但在線程對象不被回收的狀況下,好比使用線程池的時候,核心線程是一直在運行的,線程對象不會回收,如果在這樣的線程中存在上述現象,就可能出現內存泄露的問題。
那在ThreadLocalMap中是如何解決這個問題的呢?
在獲取key對應的value時,會調用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,該方法源碼以下:
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); }
經過key.threadLocalHashCode & (table.length - 1)來計算存儲key的Entry的索引位置,而後判斷對應的key是否存在,若存在,則返回其對應的value,不然,調用getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法,源碼以下:
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; }
ThreadLocalMap採用線性探查的方式來處理哈希衝突,因此會有一個while循環去查找對應的key,在查找過程當中,若發現key爲null,即經過弱引用的key被回收了,會調用expungeStaleEntry(int)方法,其源碼以下:
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
經過上述代碼能夠發現,若key爲null,則該方法經過下述代碼來清理與key對應的value以及Entry:
// expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null;
此時,CurrentThread Ref不存在一條到Entry對象的強引用鏈,Entry到value對象也不存在強引用,那在程序運行期間,它們天然也就會被回收。expungeStaleEntry(int)方法的後續代碼就是以線性探查的方式,調整後續Entry的位置,同時檢查key的有效性。
在ThreadLocalMap中的set()/getEntry()方法中,都會調用expungeStaleEntry(int)方法,可是若是咱們既不須要添加value,也不須要獲取value,那仍是有可能產生內存泄漏的。因此不少狀況下須要使用者手動調用ThreadLocal的remove()函數,手動刪除再也不須要的ThreadLocal,防止內存泄露。若對應的key存在,remove()方法也會調用expungeStaleEntry(int)方法,來刪除對應的Entry和value。
其實,最好的方式就是將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,因爲一直存在ThreadLocal的強引用,因此ThreadLocal也就不會被回收,也就能保證任什麼時候候都能根據ThreadLocal的弱引用訪問到Entry的value值,而後remove它,能夠防止內存泄露。
InheritableThreadLocal
InheritableThreadLocal繼承自ThreadLocal,使用InheritableThreadLocal類可使子線程繼承父線程的值,來看一段示例代碼:
public class ThreadLocalTest { private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() { @Override protected Integer initialValue() { return Integer.valueOf(10); } }; static class MyThread extends Thread { @Override public void run() { super.run(); System.out.println(getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get()); } } public static void main(String[] args) { System.out.println(Thread.currentThread().getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get()); MyThread myThread = new MyThread(); myThread.setName("線程A"); myThread.start(); } }
運行結果:
main inheritableThreadLocal.get() = 10 線程A inheritableThreadLocal.get() = 10
能夠看到子線程成功繼承了父線程的值。
父線程還能夠設置子線程的初始值,只須要重寫InheritableThreadLocal類的childValue(T)方法便可,將上述代碼的inheritableThreadLocal 定義修改成以下方式:
private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() { @Override protected Integer initialValue() { return Integer.valueOf(10); } @Override protected Integer childValue(Integer parentValue) { return Integer.valueOf(5); } };
運行結果爲:
main inheritableThreadLocal.get() = 10
線程A inheritableThreadLocal.get() = 5
能夠看到,子進程成功獲取到了父進程設置的初始值。
使用InheritableThreadLocal類須要注意的一點是,若是子線程在取得值的同時,主線程將InheritableThreadLocal中的值進行更改,那子線程獲取的仍是舊值。
線程中用來實現上述功能的ThreadLocalMap類變量爲
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
InheritableThreadLocal類的實現很簡單,主要是重寫了ThreadLocal類的getMap(Thread)方法和createMap(Thread, T)方法,將其中操做的ThreadLocalMap變量修改成了inheritableThreadLocals,這裏再也不進一步敘述。