Java中ThreadLocal,成員變數和區域性變數。
一.成員變數和區域性變數
1.程式例子:
public class HelloThreadTest { public static void main(String[] args) { HelloThread r = new HelloThread(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); } } class HelloThread implements Runnable { int i; @Override public void run() { while (true) { System.out.println("Hello number: " + i++); try { Thread.sleep((long) Math.random() * 1000); } catch (InterruptedException e) { e.printStackTrace(); } if (50 == i) { break; } } } }
該例子中,HelloThread類實現了Runnable介面,其中run()方法的主要工作是輸出"Hello number: "字串加數字i,並且同時遞增i,當i到達50時,退出迴圈。
main()方法中生成了一個HelloThread類的物件r,並且利用這個一個物件生成了兩個執行緒。
程式的執行結果是:順次列印了0到49的數字,共50個數字。
這是因為,i是成員變數,則HelloThread的物件r只包含這一個i,兩個Thread物件因為由r構造,所以共享了同一個i。
當我們改變程式碼如下時(原先的成員變數i被註釋掉,增加了方法中的區域性變數i):
public class HelloThreadTest { public static void main(String[] args) { HelloThread r = new HelloThread(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); } } class HelloThread implements Runnable { // int i; // 若i是成員變數,則HelloThread的物件r只包含這一個i,兩個Thread物件因為由r構造,所以共享了同一個i // 列印結果是0到49的數字 @Override public void run() { int i = 0; // 每一個執行緒都會擁有自己的一份區域性變數的拷貝 // 執行緒之間互不影響 // 所以會列印100個數字,0到49每個數字都是兩遍 while (true) { System.out.println("Hello number: " + i++); try { Thread.sleep((long) Math.random() * 1000); } catch (InterruptedException e) { e.printStackTrace(); } if (50 == i) { break; } } } }
如註釋中所述,由於區域性變數對於每一個執行緒來說都有自己的拷貝,所以各個執行緒之間不再共享同一個變數,輸出結果為100個數字,實際上是兩組,每組都是0到49的50個數字,並且兩組數字之間隨意地穿插在一起。
2.小結
如果一個變數是成員變數,那麼多個執行緒對同一個物件的成員變數進行操作時,它們對該成員變數是彼此影響的,也就是說一個執行緒對成員變數的改變會影響到另一個執行緒。
如果一個變數是區域性變數,那麼每個執行緒都會有一個該區域性變數的拷貝(即便是同一個物件中的方法的區域性變數,也會對每一個執行緒有一個拷貝),一個執行緒對該區域性變數的改變不會影響到其他執行緒。
區域性變數定義:在方法內定義的變數稱為“區域性變數”或“臨時變數”,方法結束後區域性變數佔用的記憶體將被釋放。
成員變數定義:在類體的變數部分中定義的變數,也稱為欄位。
全域性變數定義:java中沒有全域性變數的定義,全域性變數,又稱“外部變數”,它不是屬於哪個方法,作用域從定義的地址開始到原始檔結束。
注意事項:當局部變數與全域性變數重名時,起作用的是區域性變數。
二.ThreadLocal
1.ThreadLocal概述
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式。
ThreadLocal,顧名思義,它不是一個執行緒,而是執行緒的一個本地化物件。當工作於多執行緒中的物件使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒分配一個獨立的變數副本。所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其他執行緒所對應的副本。從執行緒的角度看,這個變數就像是執行緒的本地變數,這也是類名中“Local”所要表達的意思。
執行緒區域性變數並不是Java的新發明,很多語言(如IBM XL、FORTRAN)在語法層面就提供執行緒區域性變數。在Java中沒有提供語言級支援,而以一種變通的方法,通過ThreadLocal的類提供支援。所以,在Java中編寫執行緒區域性變數的程式碼相對來說要笨拙一些,這也是為什麼執行緒區域性變數沒有在Java開發者中得到很好普及的原因。
學習JDK中的類,首先看下JDK API對此類的描述,描述如下:
該類提供了執行緒區域性 (thread-local) 變數。這些變數不同於它們的普通對應物,因為訪問某個變數(通過其 get 或 set 方法)的每個執行緒都有自己的區域性變數,它獨立於變數的初始化副本。ThreadLocal 例項通常是類中的 private static 欄位,它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。
API表達了下面幾種觀點:
1、ThreadLocal不是執行緒,是執行緒的一個變數,你可以先簡單理解為執行緒類的屬性變數。
2、ThreadLocal在類中通常定義為靜態變數。
3、每個執行緒有自己的一個ThreadLocal,它是變數的一個“拷貝”,修改它不影響其他執行緒。
既然定義為類變數,為何為每個執行緒維護一個副本(姑且稱為“拷貝”容易理解),讓每個執行緒獨立訪問?多執行緒程式設計的經驗告訴我們,對於執行緒共享資源(你可以理解為屬性),資源是否被所有執行緒共享,也就是說這個資源被一個執行緒修改是否影響另一個執行緒的執行,如果影響我們需要使用synchronized同步,讓執行緒順序訪問。
ThreadLocal適用於資源共享但不需要維護狀態的情況,也就是一個執行緒對資源的修改,不影響另一個執行緒的執行;這種設計是‘空間換時間’,synchronized順序執行是‘時間換取空間’。
2. ThreadLocal方法及使用示例
ThreadLocal<T>類在Spring,Hibernate等框架中起到了很大的作用。為了解釋ThreadLocal類的工作原理,必須同時介紹與其工作甚密的其他幾個類,包括內部類ThreadLocalMap,和執行緒類Thread。所有方法如下圖:
四個核心方法說明如下:
T get() 返回此執行緒區域性變數的當前執行緒副本中的值。
protected T initialValue() 返回此執行緒區域性變數的當前執行緒的“初始值”。
void remove() 移除此執行緒區域性變數當前執行緒的值。
void set(T value) 將此執行緒區域性變數的當前執行緒副本中的值設定為指定值。
三.ThreadLocal與區域性變數
1.程式列子
public class ThreadLocalLearn {
static ThreadLocal<IntHolder> tl = new ThreadLocal<IntHolder>(){
protected IntHolder initialValue() {
return new IntHolder();
}
};
public static void main(String args[]) {
for(int i=0; i<5; i++) {
Thread th = new Thread(new ThreadTest(tl, i));
th.start();
}
}
}
class ThreadTest implements Runnable{
ThreadLocal<IntHolder> tl ; //threadlocal變數
int i; //執行緒區域性變數 (ThreadTest的成員變數)
int a = 3; //執行緒區域性變數(ThreadTest的成員變數)
public ThreadTest(ThreadLocal<IntHolder> tl, int i) {
super();
this.tl = tl;
this.i = i;
}
@Override
public void run() {
tl.get().increAndGet();
a++;
System.out.println(tl.get().getA() + " ");
System.out.println("a : " + a);
}
}
class IntHolder{
int a = 1;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public int increAndGet() {
return ++a;
}
}
程式碼的執行結果如下:
2
a : 4
2
a : 4
2
a : 4
2
a : 4
2
a : 4
可以看到,區域性變數和ThreadLocal起到的作用是一樣的,保證了併發環境下資料的安全性。
2.小結
根據上述結論那就是說,完全可以用區域性變數來代替ThreadLocal咯,這樣想法對麼?我們看一看官方對於ThreadLocal的描述:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
翻譯起來就是
ThreadLocal提供的是一種執行緒區域性變數。這些變數不同於其它變數的點在於每個執行緒在獲取變數的時候,都擁有它自己相對獨立的變數初始化拷貝。ThreadL:ocal的例項一般是私有靜態的,可以做到與一個執行緒繫結某一種狀態。PS:有更好的翻譯請指教。
所以就這段話而言,我們知道ThreadLocal不是為了滿足多執行緒安全而開發出來的,因為區域性變數已經足夠安全。ThreadLocal是為了方便執行緒處理自己的某種狀態。
可以看到ThreadLocal例項化所處的位置,是一個執行緒共有區域。好比一個銀行和個人,我們可以把錢存在銀行,也可以把錢存在家。存在家裡的錢是區域性變數,僅供個人使用;存在銀行裡的錢也不是說可以讓別人隨便使用,只有我們以個人身份去獲取才能得到。所以說ThreadLocal封裝的變數我們是在外面某個區域儲存了處於我們個人的一個狀態,只允許我們自己去訪問和修改的狀態。
ThreadLocal同時提供了初始化的機制,在例項化時重寫initialValue()方法,便可實現變數的初始化工作
但注意安全性問題
//method 1
static ThreadLocal<IntHolder> tl = new ThreadLocal<IntHolder>(){
protected IntHolder initialValue() {
return new IntHolder();
}
};
//method 2
IntHolder intHolder = new IntHolder();
static ThreadLocal<IntHolder> tl = new ThreadLocal<IntHolder>(){
protected IntHolder initialValue() {
return intHolder;
}
};
方法一和方法二都可以實現初始化工作,但是方法二不能保證執行緒變數的安全性,因為引用拷貝指向的是同一個例項,對引用拷貝的修改,等同於對例項的修改。
當然,也可以在判斷ThreadlLocal獲取資料為空時,線上程內部為ThreadLocal例項化一個數據。如下
if(null == tl.get()) {
tl.set(new IntHolder());
}