今天我們一起探討下ThreadLocal的實現原理和源碼分析。首先,本文先談一下對ThreadLocal的理解,然后根據ThreadLocal類的源碼分析了其實現原理和使用需要注意的地方,最后給出了兩個應用場景。相信本文一定能讓大家完全了解ThreadLocal。
ThreadLocal是什么?
ThreadLocal是啥?以前面試別人時就喜歡問這個,有些伙伴喜歡把它和線程同步機制混為一談,事實上ThreadLocal與線程同步無關。ThreadLocal雖然提供了一種解決多線程環境下成員變量的問題,但是它並不是解決多線程共享變量的問題。那么ThreadLocal到底是什么呢?
ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地線程”。其實,ThreadLocal並不是一個Thread,而是Thread的局部變量,也許把它命名為ThreadLocalVariable更容易讓人理解一些。線程局部變量(ThreadLocal)其實的功用非常簡單,就是為每一個使用該變量的線程都提供一個變量值的副本,是Java中一種較為特殊的線程綁定機制,是每一個線程都可以獨立地改變自己的副本,而不會和其它線程的副本沖突。
通過ThreadLocal存取的數據,總是與當前線程相關,也就是說,JVM 為每個運行的線程,綁定了私有的本地實例存取空間,從而為多線程環境常出現的並發訪問問題提供了一種隔離機制。ThreadLocal是如何做到為每一個線程維護變量的副本的呢?其實實現的思路很簡單,在ThreadLocal類中有一個Map,用於存儲每一個線程的變量的副本。概括起來說,ThreadLocal為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
API說明
1、ThreadLocal()
創建一個線程本地變量。
2、T get()
返回此線程局部變量的當前線程副本中的值,如果這是線程第一次調用該方法,則創建並初始化此副本。
3、protected T initialValue()
返回此線程局部變量的當前線程的初始值。最多在每次訪問線程來獲得每個線程局部變量時調用此方法一次,即線程第一次使用 get() 方法訪問變量的時候。如果線程先於 get 方法調用 set(T) 方法,則不會在線程中再調用 initialValue 方法。
若該實現只返回 null;如果程序員希望將線程局部變量初始化為 null 以外的某個值,則必須為 ThreadLocal 創建子類,並重寫此方法。通常,將使用匿名內部類。initialValue 的典型實現將調用一個適當的構造方法,並返回新構造的對象。
4、void remove()
移除此線程局部變量的值。這可能有助於減少線程局部變量的存儲需求。
5、void set(T value)
將此線程局部變量的當前線程副本中的值設置為指定值。
ThreadLocal使用示例
假設我們要為每個線程關聯一個唯一的序號,在每個線程周期內,我們需要多次訪問這個序號,這時我們就可以使用ThreadLocal了
1 package concurrent; 2 3 import java.util.concurrent.atomic.AtomicInteger; 4 5 /** 6 * Created by chenhao on 2018/12/03. 7 */ 8 public class ThreadLocalDemo { 9 public static void main(String []args){ 10 for(int i=0;i<5;i++){ 11 final Thread t = new Thread(){ 12 @Override 13 public void run(){ 14 System.out.println("當前線程:"+Thread.currentThread().getName()+",已分配ID:"+ThreadId.get()); 15 } 16 }; 17 t.start(); 18 } 19 } 20 static class ThreadId{ 21 //一個遞增的序列,使用AtomicInger原子變量保證線程安全 22 private static final AtomicInteger nextId = new AtomicInteger(0); 23 //線程本地變量,為每個線程關聯一個唯一的序號 24 private static final ThreadLocal<Integer> threadId = 25 new ThreadLocal<Integer>() { 26 @Override 27 protected Integer initialValue() { 28 return nextId.getAndIncrement();//相當於nextId++,由於nextId++這種操作是個復合操作而非原子操作,會有線程安全問題(可能在初始化時就獲取到相同的ID,所以使用原子變量 29 } 30 }; 31 32 //返回當前線程的唯一的序列,如果第一次get,會先調用initialValue,后面看源碼就了解了 33 public static int get() { 34 return threadId.get(); 35 } 36 } 37 }
運行結果:
當前線程:Thread-4,已分配ID:1 當前線程:Thread-0,已分配ID:0 當前線程:Thread-2,已分配ID:3 當前線程:Thread-1,已分配ID:4 當前線程:Thread-3,已分配ID:2
ThreadLocal源碼分析
ThreadLocal最常見的操作就是set、get、remove三個動作,下面來看看這三個動作到底做了什么事情。首先看set操作,源碼片段
1 public void set(T value) { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) 5 map.set(this, value); 6 else 7 createMap(t, value); 8 }
第 2 行代碼取出了當前線程 t,然后調用getMap(t)方法時傳入了當前線程,換句話說,該方法返回的ThreadLocalMap和當前線程有點關系,我們先記錄下來。進一步判定如果這個map不為空,那么設置到Map中的Key就是this,值就是外部傳入的參數。這個this是什么呢?就是定義的ThreadLocal對象。
代碼中有兩條路徑需要追蹤,分別是getMap(Thread)和createMap(Thread , T)。首先來看看getMap(t)操作
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
在這里,我們看到ThreadLocalMap其實就是線程里面的一個屬性,它在Thread類中的定義是:
ThreadLocal.ThreadLocalMap threadLocals = null;
即:每個Thread對象都有一個ThreadLocal.ThreadLocalMap成員變量,ThreadLocal.ThreadLocalMap是一個ThreadLocal類的靜態內部類(如下所示),所以Thread類可以進行引用.所以每個線程都會有一個ThreadLocal.ThreadLocalMap對象的引用
static class ThreadLocalMap {
首先獲取當前線程的引用,然后獲取當前線程的ThreadLocal.ThreadLocalMap對象,如果該對象為空就創建一個,如下所示:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
這個this變量就是ThreadLocal的引用,對於同一個ThreadLocal對象每個線程都是相同的,但是每個線程各自有一個ThreadLocal.ThreadLocalMap對象保存着各自ThreadLocal引用為key的值,所以互不影響,而且:如果你新建一個ThreadLocal的對象,這個對象還是保存在每個線程同一個ThreadLocal.ThreadLocalMap對象之中,因為一個線程只有一個ThreadLocal.ThreadLocalMap對象,這個對象是在第一個ThreadLocal第一次設值的時候進行創建,如上所述的createMap方法.
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); }
至此,ThreadLocal的原理我們應該已經清楚了,簡單來講,就是每個Thread里面有一個ThreadLocal.ThreadLocalMap threadLocals作為私有的變量而存在,所以是線程安全的。ThreadLocal通過Thread.currentThread()獲取當前的線程就能得到這個Map對象,同時將自身(ThreadLocal對象)作為Key發起寫入和讀取,由於將自身作為Key,所以一個ThreadLocal對象就能存放一個線程中對應的Java對象,通過get也自然能找到這個對象。
最后來看看get()、remove()代碼,或許看到這里就可以認定我們的理論是正確的
1 public T get() { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 ThreadLocalMap.Entry e = map.getEntry(this); 6 if (e != null) { 7 @SuppressWarnings("unchecked") 8 T result = (T)e.value; 9 return result; 10 } 11 } 12 return setInitialValue(); 13 } 14 15 public void remove() { 16 ThreadLocalMap m = getMap(Thread.currentThread()); 17 if (m != null) 18 m.remove(this); 19 }
第一句是取得當前線程,然后通過getMap(t)方法獲取到一個map,map的類型為ThreadLocalMap。然后接着下面獲取到<key,value>鍵值對,注意這里獲取鍵值對傳進去的是 this,而不是當前線程t。
如果獲取成功,則返回value值。
如果map為空,則調用setInitialValue方法返回value。
可以看出第12行處的方法setInitialValue()只有在線程第一次使用 get() 方法訪問變量的時候調用。如果線程先於 get 方法調用 set(T) 方法,則不會在線程中再調用 initialValue 方法。
protected T initialValue() { return null; }
該方法定義為protected級別且返回為null,很明顯是要子類實現它的,所以我們在使用ThreadLocal的時候一般都應該覆蓋該方法,創建匿名內部類重寫此方法。該方法不能顯示調用,只有在第一次調用get()或者set()方法時才會被執行,並且僅執行1次。
對於ThreadLocal需要注意的有兩點:
1. ThreadLocal實例本身是不存儲值,它只是提供了一個在當前線程中找到副本值得key。
2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴會弄錯他們的關系。
ThreadLocal的應用場景
最常見的ThreadLocal使用場景為 用來解決 數據庫連接、Session管理等。如:
/** * 數據庫連接管理類 */ public class ConnectionManager { /** 線程內共享Connection,ThreadLocal通常是全局的,支持泛型 */ private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>(); public static Connection getCurrConnection() { // 獲取當前線程內共享的Connection Connection conn = threadLocal.get(); try { // 判斷連接是否可用 if(conn == null || conn.isClosed()) { // 創建新的Connection賦值給conn(略) // 保存Connection threadLocal.set(conn); } } catch (SQLException e) { // 異常處理 } return conn; } /** * 關閉當前數據庫連接 */ public static void close() { // 獲取當前線程內共享的Connection Connection conn = threadLocal.get(); try { // 判斷是否已經關閉 if(conn != null && !conn.isClosed()) { // 關閉資源 conn.close(); // 移除Connection threadLocal.remove(); conn = null; } } catch (SQLException e) { // 異常處理 } } }
也可以重寫initialValue方法
private static ThreadLocal<Connection> connectionHolder= new ThreadLocal<Connection>() { public Connection initialValue() { return DriverManager.getConnection(DB_URL); } }; public static Connection getConnection() { return connectionHolder.get(); }
Hiberante的Session 工具類HibernateUtil
public class HibernateUtil { private static Log log = LogFactory.getLog(HibernateUtil.class); private static final SessionFactory sessionFactory; //定義SessionFactory static { try { // 通過默認配置文件hibernate.cfg.xml創建SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory(); } catch (Throwable ex) { log.error("初始化SessionFactory失敗!", ex); throw new ExceptionInInitializerError(ex); } } //創建線程局部變量session,用來保存Hibernate的Session public static final ThreadLocal session = new ThreadLocal(); /** * 獲取當前線程中的Session * @return Session * @throws HibernateException */ public static Session currentSession() throws HibernateException { Session s = (Session) session.get(); // 如果Session還沒有打開,則新開一個Session if (s == null) { s = sessionFactory.openSession(); session.set(s); //將新開的Session保存到線程局部變量中 } return s; } public static void closeSession() throws HibernateException { //獲取線程局部變量,並強制轉換為Session類型 Session s = (Session) session.get(); session.set(null); if (s != null) s.close(); } }
在這個類中,由於沒有重寫ThreadLocal的initialValue()方法,則首次創建線程局部變量session其初始值為null,第一次調用currentSession()的時候,線程局部變量的get()方法也為null。因此,對session做了判斷,如果為null,則新開一個Session,並保存到線程局部變量session中
ThreadLocal使用的一般步驟
1、在多線程的類(如ThreadDemo類)中,創建一個ThreadLocal對象threadXxx,用來保存線程間需要隔離處理的對象xxx。
2、在ThreadDemo類中,創建一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象為null時候,應該new()一個隔離訪問類型的對象,並強制轉換為要應用的類型。
3、在ThreadDemo類的run()方法中,通過getXxx()方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象。
ThreadLocal為什么會內存泄漏
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,說明該map的key為一個弱引用,我們知道弱引用有利於GC回收。
ThreadLocalMap
使用ThreadLocal
的弱引用作為key
,如果一個ThreadLocal
沒有外部強引用來引用它,那么系統 GC 的時候,這個ThreadLocal
勢必會被回收,這樣一來,ThreadLocalMap
中就會出現key
為null
的Entry
,就沒有辦法訪問這些key
為null
的Entry
的value
,如果當前線程再遲遲不結束的話,這些key
為null
的Entry
的value
就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,造成內存泄漏。其實,ThreadLocalMap
的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocal
的get()
,set()
,remove()
的時候都會清除線程ThreadLocalMap
里所有key
為null
的value
。但是這些被動的預防措施並不能保證不會內存泄漏:
-
使用
static
的ThreadLocal
,延長了ThreadLocal
的生命周期,可能導致的內存泄漏。 -
分配使用了
ThreadLocal
又不再調用get()
,set()
,remove()
方法,那么就會導致內存泄漏。
為什么使用弱引用
- key 使用強引用:引用的
ThreadLocal
的對象被回收了,但是ThreadLocalMap
還持有ThreadLocal
的強引用,如果沒有手動刪除,ThreadLocal
不會被回收,導致Entry
內存泄漏。 - key 使用弱引用:引用的
ThreadLocal
的對象被回收了,由於ThreadLocalMap
持有ThreadLocal
的弱引用,即使沒有手動刪除,ThreadLocal
也會被回收。value
在下一次ThreadLocalMap
調用set
,get
,remove
的時候會被清除。
1、可以知道使用弱引用可以多一層保障:理論上弱引用ThreadLocal
不會內存泄漏,對應的value
在下一次ThreadLocalMap
調用set
,get
,remove
的時候會被清除;但是如果分配使用了ThreadLocal
又不再調用get()
,set()
,remove()
方法,那么就有可能導致內存泄漏
2、通常,我們需要保證作為key的ThreadLocal類型能夠被全局訪問到,同時也必須保證其為單例,因此,在一個類中將其設為static類型便成為了慣用做法,如上面例子中都是用了Static修飾。使用static修飾ThreadLocal對象的引用后,ThreadLocal的生命周期跟Thread
一樣長,因此ThreadLocalMap的Key也不會被GC回收,弱引用形同虛設,此時就極容易造成ThreadLocalMap內存泄露。
關鍵在於threadLocal如果用Static修飾,如果是多線程操作threadlocal,當前線程結束后,ThreadLocal對象作為GCRoot還在其他線程中,這是弱引用就不能被回收,也就是當前Thread中的Map中的key還不會被回收,也就是很多線程中都有threadlocal為key的map不會被回收,那就會出現內存泄露。
ThreadLocal 最佳實踐
綜合上面的分析,我們可以理解ThreadLocal
內存泄漏的前因后果,那么怎么避免內存泄漏呢?
-
每次使用完
ThreadLocal
,都調用它的remove()
方法,清除數據。
在使用線程池的情況下,沒有及時清理ThreadLocal
,不僅是內存泄漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal
就跟加鎖完要解鎖一樣,用完就清理。
總結
- ThreadLocal 不是用於解決共享變量的問題的,也不是為了協調線程同步而存在,而是為了方便每個線程處理自己的狀態而引入的一個機制。這點至關重要。
- 每個Thread內部都有一個ThreadLocal.ThreadLocalMap類型的成員變量,該成員變量用來存儲實際的ThreadLocal變量副本。
- ThreadLocal並不是為線程保存對象的副本,它僅僅只起到一個索引的作用。它的主要木得視為每一個線程隔離一個類的實例,這個實例的作用范圍僅限於線程內部。
- 每次使用完
ThreadLocal
,都調用它的remove()
方法,清除數據,避免造成內存泄露。