【多執行緒】徹底理解ThreadLocal
阿新 • • 發佈:2019-01-25
徹底理解ThreadLocal
知其然
synchronized這類執行緒同步的機制可以解決多執行緒併發問題,在這種解決方案下,多個執行緒訪問到的,都是同一份變數的內容。為了防止在多執行緒訪問的過程中,可能會出現的併發錯誤。不得不對多個執行緒的訪問進行同步,這樣也就意味著,多個執行緒必須先後對變數的值進行訪問或者修改,這是一種以延長訪問時間來換取執行緒安全性的策略。
而ThreadLocal類為每一個執行緒都維護了自己獨有的變數拷貝。每個執行緒都擁有了自己獨立的一個變數,競爭條件被徹底消除了,那就沒有任何必要對這些執行緒進行同步,它們也能最大限度的由CPU排程,併發執行。並且由於每個執行緒在訪問該變數時,讀取和修改的,都是自己獨有的那一份變數拷貝,變數被徹底封閉在每個訪問的執行緒中,併發錯誤出現的可能也完全消除了。對比前一種方案,這是一種以空間來換取執行緒安全性的策略。
來看一個運用ThreadLocal來實現資料庫連線Connection物件執行緒隔離的例子。
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); } }
通過呼叫ConnectionManager.getConnection()方法,每個執行緒獲取到的,都是和當前執行緒繫結的那個Connection物件,第一次獲取時,是通過initialValue()方法的返回值來設定值的。通過ConnectionManager.setConnection(Connection conn)方法設定的Connection物件,也只會和當前執行緒繫結。這樣就實現了Connection物件在多個執行緒中的完全隔離。在Spring容器中管理多執行緒環境下的Connection物件時,採用的思路和以上程式碼非常相似。
知其所以然
那麼到底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);
}
程式碼已經說的非常直白,就是獲取和設定Thread內的一個叫threadLocals的變數,而這個變數的型別就是ThreadLocalMap,這樣進一步驗證了上文中的觀點:每個執行緒都有自己獨立的ThreadLocalMap物件。開啟java.lang.Thread類的原始碼,我們能得到更直觀的證明:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
那麼接下來再看一下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();
}
/**
* 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;
}
這兩個方法的程式碼告訴我們,在獲取和當前執行緒繫結的值時,ThreadLocalMap物件是以this指向的ThreadLocal物件為鍵進行查詢的,這當然和前面set()方法的程式碼是相呼應的。
進一步地,我們可以建立不同的ThreadLocal例項來實現多個變數在不同執行緒間的訪問隔離,為什麼可以這麼做?因為不同的ThreadLocal物件作為不同鍵,當然也可以線上程的ThreadLocalMap物件中設定不同的值了。通過ThreadLocal物件,在多執行緒中共享一個值和多個值的區別,就像你在一個HashMap物件中儲存一個鍵值對和多個鍵值對一樣,僅此而已。
設定到這些執行緒中的隔離變數,會不會導致記憶體洩漏呢?ThreadLocalMap物件儲存在Thread物件中,當某個執行緒終止後,儲存在其中的執行緒隔離的變數,也將作為Thread例項的垃圾被回收掉,所以完全不用擔心記憶體洩漏的問題。在多個執行緒中隔離的變數,光榮的生,合理的死,真是圓滿,不是麼?
最後再提一句,ThreadLocal變數的這種隔離策略,也不是任何情況下都能使用的。如果多個執行緒併發訪問的物件例項只允許,也只能建立那麼一個,那就沒有別的辦法了,老老實實的使用同步機制來訪問吧。
參考地址:https://my.oschina.net/lichhao/blog/111362