用ThreadLocal類實現線程安全的正確姿勢
大家通常知道,ThreadLocal類可以幫助我們實現線程的安全性,這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal提供了get與set等訪問接口或方法,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。從概念上看,我們把ThreadLocal<T>理解成一個包含了Map<Thread,T>的對象,其中Map的key用來標識不同的線程,而Map的value存放了特定該線程的某個值。但是ThreadLocal的實現并非如此,我們以這樣的理解方式去使用ThreadLocal也并不能實現真正的線程安全。
下面我們舉一個例子進行說明,Number是擁有一個int型成員變量的類:
public class Number {
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
@Override
public String toString() {
return "Number [num=" + num + "]";
}
}
NotSafeThread是一個實現了Runable接口的類,其中我們創建了一個ThreadLocal<Number>類型的變量value,用來存放不同線程的num值,接著我們用線程池的方式啟動了5個線程,我們希望使用ThreadLocal類為5個不同的線程都存放一個Number類型的副本,根除對變量的共享,并且在調用ThreadLocal類的get()方法時,返回與線程關聯的Number對象,而這些Number對象我們希望它們都能跟蹤自己的計數值:
public class NotSafeThread implements Runnable {
public static Number number = new Number();
public static int i = 0;
public void run() {
//每個線程計數加一
number.setNum(i++);
//將其存儲到ThreadLocal中
value.set(number);
//輸出num值
System.out.println(value.get().getNum());
}
public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
};
public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
newCachedThreadPool.execute(new NotSafeThread());
}
}
}
啟動程序:輸出結果
看起來一切正常,每個線程好像都有自己關于Number的存儲空間,但是我們簡單的在輸出前加一個延時:
public class NotSafeThread implements Runnable {
public static Number number = new Number();
public static int i = 0;
public void run() {
//每個線程計數加一
number.setNum(i++);
//將其存儲到ThreadLocal中
value.set(number);
//延時2秒
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
}
//輸出num值
System.out.println(value.get().getNum());
}
public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
};
public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
newCachedThreadPool.execute(new NotSafeThread());
}
}
}
運行程序,輸出:
為什么每個線程都輸出4?難道他們沒有獨自保存自己的Number副本嗎?為什么其他線程還是能夠修改這個值?我們看一下ThreadLocal的源碼:
public void set(Object obj)
{
Thread thread = Thread.currentThread();//獲取當前線程
ThreadLocalMap threadlocalmap = getMap(thread);
if(threadlocalmap != null)
threadlocalmap.set(this, obj);
else
createMap(thread, obj);
}
其中getMap方法:
ThreadLocal.ThreadLocalMap getMap(Thread thread)
{
return thread.inheritableThreadLocals;//返回的是thread的成員變量
}
可以看到,這些特定于線程的值是保存在當前的Thread對象中,并非保存在ThreadLocal對象中。并且我們發現Thread對象中保存的是Object對象的一個引用,這樣的話,當有其他線程對這個引用指向的對象做修改時,當前線程Thread對象中保存的值也會發生變化。這也就是為什么上面的程序為什么會輸出一樣的結果:5個線程中保存的是同一Number對象的引用,在線程睡眠2s的時候,其他線程將num變量進行了修改,因此它們最終輸出的結果是相同的。
那么,ThreadLocal的“為每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。”這句話中的“獨立的副本”,也就是我們理解的“線程本地存儲”只能是每個線程所獨有的對象并且不與其他線程進行共享,大概是這樣的情況:
public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
public Number initialValue(){//為每個線程保存的值進行初始化操作
return new Number();
}
};
或者
public void run() {
value.set(new Number());
}
好吧...這個時候估計你會說:那這個ThreadLocal有什么用嘛,每個線程都自己new一個對象使用,只有它自己使用這個對象而不進行共享,那么程序肯定是線程安全的咯。這樣看起來我不使用ThreadLocal,在需要用某個對象的時候,直接new一個給本線程使用不就好咯。
確實,ThreadLocal的使用不是為了能讓多個線程共同使用某一對象,而是我有一個線程A,其中我需要用到某個對象o,這個對象o在這個線程A之內會被多處調用,而我不希望將這個對象o當作參數在多個方法之間傳遞,于是,我將這個對象o放到TheadLocal中,這樣,在這個線程A之內的任何地方,只要線程A之中的方法不修改這個對象o,我都能取到同樣的這個變量o。
再舉一個在實際中應用的例子,例如,我們有一個銀行的BankDAO類和一個個人賬戶的PeopleDAO類,現在需要個人向銀行進行轉賬,在PeopleDAO類中有一個賬戶減少的方法,BankDAO類中有一個賬戶增加的方法,那么這兩個方法在調用的時候必須使用同一個Connection數據庫連接對象,如果他們使用兩個Connection對象,則會開啟兩段事務,可能出現個人賬戶減少而銀行賬戶未增加的現象。使用同一個Connection對象的話,在應用程序中可能會設置為一個全局的數據庫連接對象,從而避免在調用每個方法時都傳遞一個Connection對象。問題是當我們把Connection對象設置為全局變量時,你不能保證是否有其他線程會將這個Connection對象關閉,這樣就會出現線程安全問題。解決辦法就是在進行轉賬操作這個線程中,使用ThreadLocal中獲取Connection對象,這樣,在調用個人賬戶減少和銀行賬戶增加的線程中,就能從ThreadLocal中取到同一個Connection對象,并且這個Connection對象為轉賬操作這個線程獨有,不會被其他線程影響,保證了線程安全性。
代碼如下:
public class ConnectionHolder {
public static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
};
public static Connection getConnection(){
Connection connection = connectionHolder.get();
if(null == connection){
connection = DriverManager.getConnection(DB_URL);
connectionHolder.set(connection);
}
return connection;
}
}
在框架中,我們需要將一個事務上下文(Transaction Context)與某個執行中的線程關聯起來。通過將事務上下文保存在靜態的ThreaLocal對象中(這個上下文肯定是不與其他線程共享的),可以很容易地實現這個功能:當框架代碼需要判斷當前運行的是哪一個事務時,只需從這個ThreadLocal對象中讀取事務上下文。這種機制很方便,因為它避免了在調用每個方法時都需要傳遞執行上下文信息,然而這也將使用該機制的代碼與框架耦合在一起。
來自:http://www.cnblogs.com/qilong853/p/5982878.html