併發程式設計(四)—— ThreadLocal原始碼分析及記憶體洩露預防
今天我們一起探討下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 最佳實踐
綜合上面的分析,我們可以理解ThreadLocal
記憶體洩漏的前因後果,那麼怎麼避免記憶體洩漏呢?
-
每次使用完
ThreadLocal
,都呼叫它的remove()
方法,清除資料。
在使用執行緒池的情況下,沒有及時清理ThreadLocal
,不僅是記憶體洩漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal
就跟加鎖完要解鎖一樣,用完就清理。
總結
- ThreadLocal 不是用於解決共享變數的問題的,也不是為了協調執行緒同步而存在,而是為了方便每個執行緒處理自己的狀態而引入的一個機制。這點至關重要。
- 每個Thread內部都有一個ThreadLocal.ThreadLocalMap型別的成員變數,該成員變數用來儲存實際的ThreadLocal變數副本。
- ThreadLocal並不是為執行緒儲存物件的副本,它僅僅只起到一個索引的作用。它的主要木得視為每一個執行緒隔離一個類的例項,這個例項的作用範圍僅限於執行緒內部。
- 每次使用完
ThreadLocal
,都呼叫它的remove()
方法,清除資料,避免造成記憶體洩露。