java 對象的共享
要編寫正確的并發程序,關鍵在于在訪問共享可變狀態是需要進行正確的管理,下面介紹如何共享和發布對象,從而使他們能夠安全的有多線程同時訪問。
volatile
加鎖機制既可以確保可見性有可以確保原子性,而volatile變量只能確保可見性。
典型用法
volatile boolean asleep;
...
while (!asleep)
countSomeSheep();
非原子的64位操作
jvm起初設計的時候64位計算并不是普遍的,大部分機器還是32位的。在32位機器上計算long類型時,其實是分成高位和低位分別計算,在把結果返回。但是jvm規范并沒有強制要求這個操作時原子性的,所以在并發場景下,一個線程讀到的long可能是另一個線程只計算了高位或低位的結果。為了避免這樣的操作,需要把這個變量聲明稱volatile。
volatile long l;
發布與溢出
先看一個例子
// BAD
class UnsafeStates {
private String[] states = new String[] {"AK", "AL" ... };
public String[] getStates() { return states; }
}
上面的方法直接把內部變量引用返回,造成了內部變量溢出。正確的方式應該是:
// GOOD
class UnsafeStates {
private String[] states = new String[]{"AK", "AL"};
public String[] getStates() {
// 返回副本,這樣就不會影響內部
String[] tmp = new String[states.length];
System.arraycopy(states, 0, tmp, 0, states.length);
return tmp;
}
}
封接能夠使得對程序的可見性進行分析變得可能,并使得無意中破壞設計約束條件變得更雄。
// BAD
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
上面的例子隱式的提前暴露了this對象(在對象構造完成之前,或者說在構造方法中暴露了當前對象的引用)。在構造方法完成之前,當前對象的處于不可預測和不一致的狀態,this對象提前暴露,超出了它的所用范圍。正確的方法是可以把對象引用保存到變量中,等構造方法完成后在調用,如下;
// GOOD
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
安全對象的構造過程
線程封閉
當訪問共享的可變數據室,通常需要使用同步。一種避免使用同步的方式就是不同享數據。如果僅在但相成內訪問數據,就不需要同步。折中技術被稱為線程封閉(Thead Confinement),他是實現線程安全性的最簡單方式之一。
一種常見的應用是JDBC的Connection對象。JDBC規范并不要求Connecting對象必須是線程安全的。在典型的服務器應用程序中,線程從連接池中獲得一個Connectiion對象,并且用改對象來處理請求,使用完成后在將對象返回給連接池。由于大多數請求(servlet請求)都是有單個線程采用同步方式來處理,并且在Connection對象返回之前,連接池不會再將它分配給其他線程,因此,折中連接管理模式在處理請求是隱含地將Connection對象封閉在線程中。
Java語言中無法強制將對象封閉在某個線程中,這是程序設計需要考慮的因素。Java好辛苦提供了一些機制幫助維持線程封閉性,例如局部變量和ThreadLocal類,即便如此,程序員仍然需要負責確保封閉在線程中的對象不會從線程中溢出。
Ad-hoc 線程封閉
維護線程封閉性的職責完全由程序實現來承擔,ad-hoc線程封閉式非常脆弱的,因為沒有任何一種語言特性支持他。事實上,對線程封閉對象(例如,GUI應用程序中的可視化組建或數據模型等)的引用通常保存在公有變量中。
由于Ad-hoc線程封閉技術的脆弱性,因此在線程中盡量少用他,在可能的情況下,應該使用更強的線程封閉技術(例如,棧封閉或ThreadLocal類)。
棧封閉
棧封閉式一個對象只能通過本地變量訪問。就像封裝更容易保存不變量,本地變量可以更容易限定線程對變量的訪問。本地變量的本質是限定在當前線程內,他存在于執行線程棧中,不能被其他線程訪問。棧封閉(又叫 within-thread 或 thread-local,不要和ThreadLocal類混了)更容易維護并且比Ad-hoc線程封閉更強壯。
通俗一點講,本地變量是變量作用域最小的,在開發多線程程序是應該盡量減小變量的作用域。
不變性
如果一個對象發布以后不會發生變化,那么在訪問他的時候就不用考慮線程安全問題了。
**不可變對象一定是線程安全的 **
當滿足一下條件時,對象才是不可變的:
- 對象創建以后七狀態就不能I許改。
- 對象的所有與都是final類型
- 對象是正確創建的(在對象的創建其間,this引用沒有溢出)。
安全發布
上節講的是如何把對象封閉在線程或另一個對象的內部,確保對象不被發布。
要安全地發布一個對象,對象的引用以及對象的狀態必須同時對其他線程可見。一個正確構造的對象可以通過一下方式來安全的發布:
- 在靜態初始化函數中初始化一個對象引用
- 講對象的引用保存到volatile類型的域或者AtomicRefer對象中。
- 講對象的引用保存到謳歌正確構造對象的final類型域中g- 講對象的引用保存到一個有鎖保護的域中。
可變對象在構造后可以,那么安全發布只能確保“發布當時”狀態的可見性。
對象的發布需求取決于他的可變性:
- 不可變對象可以通過任意機制來發布。
- 事實不可變對象必須通過安全方式來發布
- 可變對象必須通過安全方式來發布,必須通過是線程安全的或者有某個鎖保護起來。
在并發程序中使用和共享對象是,可以使用一些實用的策略,包括:
線程封閉。線程封閉的對象只能由一個線程擁有,對象被封閉在改線程中,并且只能由一個線程修改。
制度共享。在沒有額外同步的情況下,共享的制度對象可以有多線程并發訪問,但任何線程都不能修改I啊它。共享的制度對象包括不可變對象和事實不可變對戲那個。
線程安全共享。線程安全的對象在其內部實現同步,因此多個線程可以通過對象的共有接口進行訪問和不需要進一步的同步。
保護對象。被保護的對象只能通過持有特定的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及已發布的并且由某個特定鎖保護的對象。