java 對象的共享
要編寫正確的并發(fā)程序革半,關(guān)鍵在于在訪問共享可變狀態(tài)是需要進(jìn)行正確的管理,下面介紹如何共享和發(fā)布對象,從而使他們能夠安全的有多線程同時(shí)訪問虾攻。
volatile
加鎖機(jī)制既可以確保可見性有可以確保原子性更鲁,而volatile變量只能確宾浚可見性。
典型用法
volatile boolean asleep;
...
while (!asleep)
countSomeSheep();
非原子的64位操作
jvm起初設(shè)計(jì)的時(shí)候64位計(jì)算并不是普遍的澡为,大部分機(jī)器還是32位的朋沮。在32位機(jī)器上計(jì)算long類型時(shí),其實(shí)是分成高位和低位分別計(jì)算缀壤,在把結(jié)果返回樊拓。但是jvm規(guī)范并沒有強(qiáng)制要求這個(gè)操作時(shí)原子性的,所以在并發(fā)場景下塘慕,一個(gè)線程讀到的long可能是另一個(gè)線程只計(jì)算了高位或低位的結(jié)果筋夏。為了避免這樣的操作,需要把這個(gè)變量聲明稱volatile图呢。
volatile long l;
發(fā)布與溢出
先看一個(gè)例子
// BAD
class UnsafeStates {
private String[] states = new String[] {"AK", "AL" ... };
public String[] getStates() { return states; }
}
上面的方法直接把內(nèi)部變量引用返回条篷,造成了內(nèi)部變量溢出骗随。正確的方式應(yīng)該是:
// GOOD
class UnsafeStates {
private String[] states = new String[]{"AK", "AL"};
public String[] getStates() {
// 返回副本,這樣就不會影響內(nèi)部
String[] tmp = new String[states.length];
System.arraycopy(states, 0, tmp, 0, states.length);
return tmp;
}
}
封接能夠使得對程序的可見性進(jìn)行分析變得可能赴叹,并使得無意中破壞設(shè)計(jì)約束條件變得更雄鸿染。
// BAD
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
上面的例子隱式的提前暴露了this對象(在對象構(gòu)造完成之前,或者說在構(gòu)造方法中暴露了當(dāng)前對象的引用)乞巧。在構(gòu)造方法完成之前涨椒,當(dāng)前對象的處于不可預(yù)測和不一致的狀態(tài),this對象提前暴露绽媒,超出了它的所用范圍蚕冬。正確的方法是可以把對象引用保存到變量中,等構(gòu)造方法完成后在調(diào)用是辕,如下囤热;
// 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;
}
}
安全對象的構(gòu)造過程
線程封閉
當(dāng)訪問共享的可變數(shù)據(jù)室,通常需要使用同步获三。一種避免使用同步的方式就是不同享數(shù)據(jù)旁蔼。如果僅在但相成內(nèi)訪問數(shù)據(jù),就不需要同步疙教。折中技術(shù)被稱為線程封閉(Thead Confinement)棺聊,他是實(shí)現(xiàn)線程安全性的最簡單方式之一。
一種常見的應(yīng)用是JDBC的Connection對象松逊。JDBC規(guī)范并不要求Connecting對象必須是線程安全的躺屁。在典型的服務(wù)器應(yīng)用程序中,線程從連接池中獲得一個(gè)Connectiion對象经宏,并且用改對象來處理請求犀暑,使用完成后在將對象返回給連接池。由于大多數(shù)請求(servlet請求)都是有單個(gè)線程采用同步方式來處理烁兰,并且在Connection對象返回之前耐亏,連接池不會再將它分配給其他線程,因此沪斟,折中連接管理模式在處理請求是隱含地將Connection對象封閉在線程中广辰。
Java語言中無法強(qiáng)制將對象封閉在某個(gè)線程中,這是程序設(shè)計(jì)需要考慮的因素主之。Java好辛苦提供了一些機(jī)制幫助維持線程封閉性择吊,例如局部變量和ThreadLocal類,即便如此槽奕,程序員仍然需要負(fù)責(zé)確保封閉在線程中的對象不會從線程中溢出几睛。
Ad-hoc 線程封閉
維護(hù)線程封閉性的職責(zé)完全由程序?qū)崿F(xiàn)來承擔(dān),ad-hoc線程封閉式非常脆弱的粤攒,因?yàn)闆]有任何一種語言特性支持他所森。事實(shí)上囱持,對線程封閉對象(例如,GUI應(yīng)用程序中的可視化組建或數(shù)據(jù)模型等)的引用通常保存在公有變量中焕济。
由于Ad-hoc線程封閉技術(shù)的脆弱性纷妆,因此在線程中盡量少用他,在可能的情況下晴弃,應(yīng)該使用更強(qiáng)的線程封閉技術(shù)(例如掩幢,棧封閉或ThreadLocal類)。
棧封閉
棧封閉式一個(gè)對象只能通過本地變量訪問肝匆。就像封裝更容易保存不變量粒蜈,本地變量可以更容易限定線程對變量的訪問顺献。本地變量的本質(zhì)是限定在當(dāng)前線程內(nèi)旗国,他存在于執(zhí)行線程棧中,不能被其他線程訪問注整。棧封閉(又叫 within-thread 或 thread-local能曾,不要和ThreadLocal類混了)更容易維護(hù)并且比Ad-hoc線程封閉更強(qiáng)壯。
通俗一點(diǎn)講肿轨,本地變量是變量作用域最小的寿冕,在開發(fā)多線程程序是應(yīng)該盡量減小變量的作用域。
不變性
如果一個(gè)對象發(fā)布以后不會發(fā)生變化椒袍,那么在訪問他的時(shí)候就不用考慮線程安全問題了驼唱。
**不可變對象一定是線程安全的 **
當(dāng)滿足一下條件時(shí),對象才是不可變的:
- 對象創(chuàng)建以后七狀態(tài)就不能I許改驹暑。
- 對象的所有與都是final類型
- 對象是正確創(chuàng)建的(在對象的創(chuàng)建其間玫恳,this引用沒有溢出)。
安全發(fā)布
上節(jié)講的是如何把對象封閉在線程或另一個(gè)對象的內(nèi)部优俘,確保對象不被發(fā)布京办。
要安全地發(fā)布一個(gè)對象,對象的引用以及對象的狀態(tài)必須同時(shí)對其他線程可見帆焕。一個(gè)正確構(gòu)造的對象可以通過一下方式來安全的發(fā)布:
- 在靜態(tài)初始化函數(shù)中初始化一個(gè)對象引用
- 講對象的引用保存到volatile類型的域或者AtomicRefer對象中惭婿。
- 講對象的引用保存到謳歌正確構(gòu)造對象的final類型域中g(shù)- 講對象的引用保存到一個(gè)有鎖保護(hù)的域中。
可變對象在構(gòu)造后可以叶雹,那么安全發(fā)布只能確辈萍ⅲ“發(fā)布當(dāng)時(shí)”狀態(tài)的可見性。
對象的發(fā)布需求取決于他的可變性:
- 不可變對象可以通過任意機(jī)制來發(fā)布折晦。
- 事實(shí)不可變對象必須通過安全方式來發(fā)布
- 可變對象必須通過安全方式來發(fā)布钥星,必須通過是線程安全的或者有某個(gè)鎖保護(hù)起來。
在并發(fā)程序中使用和共享對象是筋遭,可以使用一些實(shí)用的策略打颤,包括:
線程封閉暴拄。線程封閉的對象只能由一個(gè)線程擁有,對象被封閉在改線程中编饺,并且只能由一個(gè)線程修改乖篷。
制度共享。在沒有額外同步的情況下透且,共享的制度對象可以有多線程并發(fā)訪問撕蔼,但任何線程都不能修改I啊它。共享的制度對象包括不可變對象和事實(shí)不可變對戲那個(gè)秽誊。
線程安全共享鲸沮。線程安全的對象在其內(nèi)部實(shí)現(xiàn)同步,因此多個(gè)線程可以通過對象的共有接口進(jìn)行訪問和不需要進(jìn)一步的同步锅论。
保護(hù)對象讼溺。被保護(hù)的對象只能通過持有特定的鎖來訪問。保護(hù)對象包括封裝在其他線程安全對象中的對象最易,以及已發(fā)布的并且由某個(gè)特定鎖保護(hù)的對象怒坯。