Java之美[從菜鳥到高手演練]之ThreadLocal原理分析
作者:二青
簡介
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式。當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。從執行緒的角度看,目標變數就象是執行緒的本地變數,這也是類名中“Local”所要表達的意思。所以,在Java中編寫執行緒區域性變數的程式碼相對來說要笨拙一些,因此造成執行緒區域性變數沒有在Java開發者中得到很好的普及。引自@
和鎖的比較
為什麼要先強調這一點,因為從簡介上看,容易使人們聯想到ThreadLocal似乎是一種解決Java多執行緒環境中執行緒同步與執行緒安全方法,其實不然,這裡要設計到兩個概念:執行緒安全,執行緒同步,事實上,ThreadLocal只解決執行緒安全的問題,並不能解決執行緒同步的問題,這也造成在剛學習的時候,我總是在苦想,ThreadLocal既然為每個執行緒拷貝一份變數,那怎麼再進行同步呢?查了很多資料後才想明白,ThreadLocal並不是用來解決執行緒同步的,所以它與鎖可以說是沒有什麼關係的,彼此各有所長,不能代替。有人這樣描述:ThreadLocal解決的是同一個執行緒內的資源共享問題,而synchronized 解決的是多個執行緒間的資源共享問題,我覺得是有道理的!推薦讀者看一下討論貼,是關於synchronized關鍵字和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。
值得一提的是,在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]
考察輸出的結果資訊,我們發現每個執行緒所產生的序號雖然都共享同一個TestNum例項,但它們並沒有發生相互干擾的情況,而是各自產生獨立的序列號,這是因為我們通過ThreadLocal為每一個執行緒提供了單獨的副本。
Thread同步機制的比較
ThreadLocal和執行緒同步機制相比有什麼優勢呢?ThreadLocal和執行緒同步機制都是為了解決多執行緒中相同變數的訪問衝突問題。在同步機制中,通過物件的鎖機制保證同一時間只有一個執行緒訪問變數。這時該變數是多個執行緒共享的,使用同步機制要求程式慎密地分析什麼時候對變數進行讀寫,什麼時候需要鎖定某個物件,什麼時候釋放物件鎖等繁雜的問題,程式設計和編寫難度相對較大。而ThreadLocal則從另一個角度來解決多執行緒的併發訪問。ThreadLocal會為每一個執行緒提供一個獨立的變數副本,從而隔離了多個執行緒對資料的訪問衝突。因為每一個執行緒都擁有自己的變數副本,從而也就沒有必要對該變數進行同步了。ThreadLocal提供了執行緒安全的共享物件,在編寫多執行緒程式碼時,可以把不安全的變數封裝進ThreadLocal。由於ThreadLocal中可以持有任何型別的物件,低版本JDK所提供的get()返回的是Object物件,需要強制型別轉換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用,程式碼清單 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。
概括起來說,對於多執行緒資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。Spring使用ThreadLocal解決執行緒安全問題我們知道在一般情況下,只有無狀態的Bean才可以在多執行緒環境下共享,在Spring中,絕大部分Bean都可以宣告為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非執行緒安全狀態採用ThreadLocal進行處理,讓它們也成為執行緒安全的狀態,因為有狀態的Bean就可以在多執行緒中共享了。一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過介面向上層開放功能呼叫。在一般情況下,從接收請求到返回響應所經過的所有程式呼叫都同屬於一個執行緒,如下圖所示:
同一執行緒貫通三層這樣你就可以根據需要,將一些非執行緒安全的變數以ThreadLocal存放,在同一次請求響應的呼叫執行緒中,所有關聯的物件引用到的都是同一個變數。下面的例項能夠體現Spring對有狀態Bean的改造思路:程式碼清單3 TestDao:非執行緒安全
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();// ②引用非執行緒安全變數
// …
}
}
由於①處的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的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();
}
再來看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;
}