早在Java 1.2推出之時,Java平台中就引入了一個新的支援:java.lang.ThreadLocal,給我們在編寫多線程程式時提供了一種新的選擇。使用這個工具類可以很簡潔地編寫出優美的多線程程式,雖然ThreadLocal非常有用,但是似乎現在了解它、使用它的朋友還不多。
ThreadLocal是什麼
ThreadLocal是什麼呢?其實ThreadLocal并非是一個線程的本地實作版本,它并不是一個Thread,而是thread local variable(線程局部變量)。也許把它命名為ThreadLocalVar更加合适。線程局部變量(ThreadLocal)其實的功用非常簡單,就是為每一個使用該變量的線程都提供一個變量值的副本,是每一個線程都可以獨立地改變自己的副本,而不會和其它線程的副本沖突。從線程的角度看,就好像每一個線程都完全擁有該變量。線程局部變量并不是Java的新發明,在其它的一些語言編譯器實作(如IBM XL FORTRAN)中,它在語言的層次提供了直接的支援。因為Java中沒有提供在語言層次的直接支援,而是提供了一個ThreadLocal的類來提供支援,是以,在Java中編寫線程局部變量的代碼相對比較笨拙,這也許是線程局部變量沒有在Java中得到很好的普及的一個原因吧。
ThreadLocal的設計
首先看看ThreadLocal的接口:
Object get() ; // 傳回目前線程的線程局部變量副本
protected Object initialValue(); // 傳回該線程局部變量的目前線程的初始值
void set(Object value); // 設定目前線程的線程局部變量副本的值
ThreadLocal有3個方法,其中值得注意的是initialValue(),該方法是一個protected的方法,顯然是為了子類重寫而特意實作的。該方法傳回目前線程在該線程局部變量的初始值,這個方法是一個延遲調用方法,在一個線程第1次調用get()或者set(Object)時才執行,并且僅執行1次。ThreadLocal中的确實實作直接傳回一個null:
protected Object initialValue() { return null; }
ThreadLocal是如何做到為每一個線程維護變量的副本的呢?其實實作的思路很簡單,在ThreadLocal類中有一個Map,用于存儲每一個線程的變量的副本。
可以看一下ThreadLocal的源碼:
ThreadLocal.set()方法: 以該Thread對象為key,将給定的value存放到該Thread的ThreadLocalMap對象中.
/**
* 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);//擷取該Thread的ThreadLocalMap對象
if (map != null)
map.set(this, value);
else
createMap(t, value); //如果ThreadLocalMap對象為空,則建立
}
ThredLocal.createMap(t,value)方法:
/**
* 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);
}
看看ThreadLocalMap的構造函數: 将給定的<firstKey,firstValue>作為第一條entry存放到map中。
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
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.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();
}
例子
比如下面的示例實作:
說了這麼多,估計還不如看下例子吧,下面還是上例子。
1.沒使用ThreadLocal的例子:
public class TestWithoutThreadLocal extends Thread {
static Integer num;
//private static ThreadLocal<Integer> num=new ThreadLocal<Integer>();
@Override
public void run(){
for(int i=0;i<5;i++){
if(num==null){
System.out.println("thread="+this.getName()+" is initing num!");
num=new Integer(0);
}
num=num+1;
System.out.println("thread="+this.getName()+",num="+num);
}
}
public static void main(String[] args) {
TestWithoutThreadLocal t1=new TestWithoutThreadLocal();
t1.setName("t1");
TestWithoutThreadLocal t2=new TestWithoutThreadLocal();
t2.setName("t2");
t1.start();
t2.start();
}
}
執行結果如下,線程t1,和t2都對變量進行了初始化,結果并沒有向程式設計的那樣,列印出1到10, 而是發生了不安全的情況。
thread=t1 is initing num!
thread=t2 is initing num!
<span style="color:#FF0000;">thread=t1,num=1</span>
thread=t1,num=2
thread=t1,num=3
thread=t1,num=4
thread=t1,num=5
<span style="color:#FF0000;">thread=t2,num=1</span>
thread=t2,num=6
thread=t2,num=7
thread=t2,num=8
thread=t2,num=9
2.使用了ThreadLocal的例子
public class TestWithoutThreadLocal extends Thread {
//static Integer num;
private static ThreadLocal<Integer> num=new ThreadLocal<Integer>();
@Override
public void run(){
for(int i=0;i<5;i++){
try {
if(this.getName().equalsIgnoreCase("t1")){
Thread.sleep(10);
}else{
Thread.sleep(3);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(num==null){
System.out.println("null");
num=new Integer(0);
}
num=num+1;
System.out.println("thread="+this.getName()+",num="+num);
}
}
public static void main(String[] args) {
TestWithoutThreadLocal t1=new TestWithoutThreadLocal();
t1.setName("t1");
TestWithoutThreadLocal t2=new TestWithoutThreadLocal();
t2.setName("t2");
t1.start();
t2.start();
}
}
執行結果:線程t1,t2各自管理自己的靜态變量num,結果符合預期
thread=t1 is initing num!
thread=t2 is initing num!
thread=t1,num=1
thread=t2,num=1
thread=t2,num=2
thread=t2,num=3
thread=t2,num=4
thread=t2,num=5
thread=t1,num=2
thread=t1,num=3
thread=t1,num=4
thread=t1,num=5
本文主要參考了這篇文章: http://www.jcwcn.com/article-30276-1.html
還有一篇文章,關于ThreadLocal的體會,寫的很好:http://lkf520java.iteye.com/blog/617368
寫這篇文章的目的不是為了來剖析ThreadLocal,因為壇子裡有許多高手已經深入淺出的把ThreadLocal講解的很清楚了。
特别是lujh99的正确了解ThreadLocal這篇文章,通過JDK源代碼把ThreadLocal講得非常深入淺出,讓我深受啟發。我寫這篇文章的目的隻是為再此作一個補充,想以另外一種通俗易懂的表達方式把自己對ThreadLocal了解寫出來。
lujh99 寫道 總之,ThreadLocal不是用來解決對象共享通路問題的,而主要是提供了保持對象的方法和避免參數傳遞的友善的對象通路方式。歸納了兩點:
1。每個線程中都有一個自己的ThreadLocalMap類對象,可以将線程自己的對象保持到其中,各管各的,線程可以正确的通路到自己的對象。
2。将一個共用的ThreadLocal靜态執行個體作為key,将不同對象的引用儲存到不同線程的ThreadLocalMap中,然後線上程執行的各處通過這個靜态ThreadLocal執行個體的get()方法取得自己線程儲存的那個對象,避免了将這個對象作為參數傳遞的麻煩。
正如lujh99 所言,ThreadLocal不是用來解決對象共享通路問題的,而是為了處理在多線程環境中,某個方法處理一個業務,需要遞歸依賴其他方法時,而要在這些方法中共享參數的問題。例如有方法a(),在該方法中調用了方法b(),而在b方法中又調用了方法c(),即a-->b--->c,如果a,b,c都需要使用使用者對象,那麼我們常用做法就是a(User user)-->b(User user)---c(User user)。但是如果使用ThreadLocal我們就可以用另外一種方式解決:
- 在某個接口中定義一個靜态的ThreadLocal 對象,例如 public static ThreadLocal threadLocal=new ThreadLocal ();
- 然後讓a,b,c方法所在的類假設是類A,類B,類C都實作1中的接口
- 在調用a時,使用A.threadLocal.set(user) 把user對象放入ThreadLocal環境
- 這樣我們在方法a,方法b,方法c可以在不用傳參數的前提下,在方法體中使用threadLocal.get()方法就可以得到user對象。
上面的類A,類B ,類C就可以分别對應我們做web開發時的 web層的Action--->業務邏輯層的Service-->資料通路層的DAO,當我們要在這三層中共享參數時,那麼我們就可以使用ThreadLocal 了。
那麼user對象是如何存放到ThreadLocal 中的?
lujh99 寫道 将一個共用的ThreadLocal靜态執行個體作為key,将不同對象的引用儲存到不同線程的ThreadLocalMap中,然後線上程執行的各處通過這個靜态ThreadLocal執行個體的get()方法取得自己線程儲存的那個對象,避免了将這個對象作為參數傳遞的麻煩。
經過我的測試,正是如此,當我們調用A.threadLocal.set(user) 時,set()做了以下幾件事:
- 得到目前線程Thread 對象
- 通過目前線程對象得到目前線程的ThreadLocalMap 對象
- 将ThreadLocal靜态執行個體作為key,user對象作值,存放到ThreadLocalMap 中。
PS:因為ThreadLocal是靜态的,是以每個線程中的ThreadLocalMap的key都是相同的,不同的隻是存放的容器ThreadLocalMap。
總結:
其實ThreadLocal 跟我們做web開發時使用的session對象的作用很類似,每當我們向伺服器發送一個請求時,web伺服器會為該請求建立一個線程,同時會為該請求建立一系列對象,其中包括session(當然在同一個浏覽器發送的請求都獲得是同一個session對象),是以當我們做web開發時,可以不用擔心線程安全問題,自由的往session中存取變量,儲存使用者狀态。同理,當我們在程式中第一次使用A.threadLocal.set(user) 存放參數時,不管在程式的哪個地方,我們都可以通過ThreadLocal 所在的接口通路靜态threadLocal對象,同時來共享threadLocal存放的參數,threadLocal就相當于session的作用,來儲存目前線程的狀态。在我們開發實際開發中,可以任意往threadLocal中共享和存取自己需要的變量,隻不過web中的session的生命周期在于用戶端的浏覽器,而threadLocal中存儲的變量的生命周期隻在于目前線程,目前結束,threadLocal中存放的參數也被銷毀。