一场仲、線程安全性
在線程安全性中法竞,最核心的概念是正確性,而正確性的含義是:某個(gè)類的行為與其規(guī)范完全一致参歹。這里的規(guī)范可以粗略理解為在各種限定條件下仰楚,類對(duì)象的結(jié)果與預(yù)期一致。在單線程中犬庇,正確性可以近似的定義為“所見即所知(we know it when we see it)”僧界。在大概明確了“安全性”的概念后,我們可以認(rèn)為線程安全性就是:當(dāng)多個(gè)線程訪問某個(gè)類時(shí)臭挽,這個(gè)類始終都能表現(xiàn)出正確的行為捂襟,那么這個(gè)類就可以認(rèn)為是線程安全的。
當(dāng)多個(gè)線程訪問某個(gè)類時(shí)埋哟,不管運(yùn)行環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行笆豁,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同郎汪,這個(gè)類都能表現(xiàn)出正確的行為,那么就稱這個(gè)類是線程安全的闯狱。
也可以將線程安全類認(rèn)為是一個(gè)在并發(fā)環(huán)境和單線程環(huán)境中都不會(huì)被破壞的類煞赢。如果某個(gè)類在單線程環(huán)境下都不是線程安全類,那么它肯定不是線程安全類哄孤。下面是一個(gè)線程安全類的示例:
public class StatelessFactorizer implements Servlet{
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
這個(gè)StatelessFactorizer是無狀態(tài)的:它既不包含任何域照筑,也不包含任何對(duì)其他類中域的引用。方法中的局部變量只能由正在執(zhí)行的線程訪問瘦陈。如果同時(shí)有多個(gè)線程在訪問StatelessFactorizer凝危,那么這些線程之間將不會(huì)互相影響,因?yàn)榫€程之間并沒有共享狀態(tài)晨逝,就好像在訪問不同的實(shí)例蛾默。
由于線程訪問無狀態(tài)對(duì)象的行為并不會(huì)影響其他線程中操作的正確性,因此無狀態(tài)對(duì)象是線程安全的捉貌,且無狀態(tài)對(duì)象一定是線程安全的支鸡。
二、原子性
什么是原子性呢趁窃?其實(shí)原子性就是一個(gè)不可再分割的性質(zhì)牧挣,不能再分成更細(xì)的粒度。
如果我們?cè)趧倓偟氖纠性黾右粋€(gè)狀態(tài)(既一個(gè)計(jì)數(shù)器)醒陆,用來統(tǒng)計(jì)已處理請(qǐng)求數(shù)量瀑构,每處理一個(gè)請(qǐng)求就將這個(gè)值加1,程序如下所示:
public class StatelessFactorizer implements Servlet{
private long count = 0;
public long getCount(){return count;}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}
在上面的程序示例中刨摩,咋一看沒問題寺晌,++count看起來像是一個(gè)操作,但是這個(gè)自增操作并非原子性的码邻。因?yàn)閷?shí)際上折剃,它包含了三個(gè)操作:“讀取-修改-寫入”的操作序列。每個(gè)操作都依賴于前面之前的狀態(tài)像屋。如果此時(shí)有兩個(gè)線程A怕犁、B,如果A線程已經(jīng)進(jìn)行到了修改操作己莺,此時(shí)如果B線程進(jìn)行了讀取奏甫,那么最終A、B線程寫入的值是一樣的凌受,這樣就與預(yù)期結(jié)果偏差了1.
雖然在這里看起來阵子,結(jié)果偏離了一些可以接受,但是如果這個(gè)計(jì)數(shù)器的值被用來生成數(shù)值序列或唯一的對(duì)象標(biāo)識(shí)符胜蛉,那么在多次調(diào)用中返回相同的值將導(dǎo)致嚴(yán)重的數(shù)據(jù)完整性問題挠进。
在并發(fā)編程中色乾,像這種由于不恰當(dāng)?shù)膱?zhí)行時(shí)序而出現(xiàn)不正確的結(jié)果是一種非常重要的情況,這種情況叫做“競態(tài)條件(Race Condition)”领突。
競態(tài)條件
當(dāng)某個(gè)計(jì)算的正確性取決于多個(gè)線程的交替執(zhí)行時(shí)序的時(shí)候暖璧,那么就會(huì)發(fā)生競態(tài)條件。常見的競態(tài)條件類型是“先檢查后執(zhí)行”操作君旦,既通過一個(gè)可能失效的觀測結(jié)果來決定下一步的動(dòng)作澎办。
舉個(gè)栗子:你和朋友約好一起去網(wǎng)吧開黑,你當(dāng)了網(wǎng)吧的時(shí)候金砍,發(fā)現(xiàn)你朋友不在局蚀,此時(shí)你可能選擇呆在網(wǎng)吧里等他,也可能去他家找他恕稠,如果你去找他琅绅,那么當(dāng)你出了網(wǎng)吧以后,你在網(wǎng)吧的觀測結(jié)果(朋友不在)就可能失效了谱俭,因?yàn)樗赡茉谀闳ニ艺宜穆飞弦呀?jīng)到了網(wǎng)吧奉件,而你卻去找他了宵蛀。
這個(gè)栗子中昆著,正確的結(jié)果是(你們?cè)诰W(wǎng)吧會(huì)面),但是這個(gè)結(jié)果取決于事件發(fā)生的時(shí)序(既誰先到網(wǎng)吧并且等待對(duì)方的時(shí)長)术陶。這種觀察結(jié)果的失效就是大多數(shù)競態(tài)條件的本質(zhì)——基于一種可能失效的觀測結(jié)果來做出判斷或者執(zhí)行某個(gè)計(jì)算凑懂。
再舉個(gè)栗子,假設(shè)有兩個(gè)線程A梧宫、B接谨,A、B線程都用來判斷某個(gè)文件夾是否存在塘匣,不存在就創(chuàng)建它脓豪,假如當(dāng)A線程發(fā)現(xiàn)文件夾不存在時(shí),正打算創(chuàng)建文件夾忌卤,但是此時(shí)B線程已經(jīng)完成了文件夾的創(chuàng)建扫夜,那么此時(shí)A線程觀測的結(jié)果就已經(jīng)失效了,但是A線程依舊根據(jù)這個(gè)已失效的觀測結(jié)果在進(jìn)行下一步動(dòng)作驰徊,這就可能會(huì)導(dǎo)致各種問題笤闯。
使用“先檢查后執(zhí)行”的一種常見的情況就是延遲初始化。就比如在單例模式中有一種寫法如下:
public class LazyInitRace {
private static LazyInitRace instance = null;
public LazyInitRace getInstance(){
if(instance == null){
instance = new LazyInitRace();
}
return instance;
}
}
這就是典型的延遲初始化棍厂,在單線程中這樣寫沒毛病颗味,但是在多線程環(huán)境中,如果有A牺弹、B線程同時(shí)執(zhí)行g(shù)etInstance()方法浦马,那么結(jié)果可能符合預(yù)期时呀,也可能會(huì)得到兩個(gè)不一樣的對(duì)象。因?yàn)樵贏線程發(fā)現(xiàn)instace為null時(shí)晶默,B線程可能也同時(shí)發(fā)現(xiàn)instace為null退唠。
與大多數(shù)并發(fā)錯(cuò)誤一樣,競態(tài)條件并不總是會(huì)產(chǎn)生錯(cuò)誤荤胁,還需要某種不恰當(dāng)?shù)膱?zhí)行時(shí)序瞧预,但是如果發(fā)生問題,那么可能導(dǎo)致很嚴(yán)重的問題仅政。
在上面的示例中都包含了一組需要以原子方式執(zhí)行(或者說不可分割)的操作垢油。要避免競態(tài)條件問題,就必須在某個(gè)線程修改變量時(shí)圆丹,通過某種方式防止其他線程使用這個(gè)變量滩愁,從而確保其他線程只能在修改操作完成之前或之后讀取和修改狀態(tài),而不是在修改過程中辫封。
在上面統(tǒng)計(jì)已處理請(qǐng)求數(shù)量的示例中硝枉,我們可以使用AtomicLong對(duì)象來替換long,因?yàn)锳tmoicLong類是線程安全類倦微,所以可以保證示例也是示例安全的妻味,但是在添加一個(gè)狀態(tài)變量時(shí),是否還可以通過使用線程安全的對(duì)象來管理而類的狀態(tài)以維護(hù)其線程安全性呢欣福?如下所示:
public class UnsafeCachingFactorizer implements Servlet {
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber.get())) {
encodeIntoResponse(resp, lastFactors.get());
} else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
在上述例子中责球,雖然兩個(gè)變量都是線程安全的,但是在service方法中依然存在競態(tài)條件拓劝,因?yàn)樵谏鲜隼又谐猓惖牟蛔冃詶l件已經(jīng)被破壞了,只有確保了這個(gè)不變性條件不被破壞郑临,才是正確的栖博。當(dāng)不變性條件中涉及到了多個(gè)變量時(shí),各個(gè)變量之間并不是彼此獨(dú)立的厢洞,而是某個(gè)變量的值會(huì)對(duì)其他變量的值產(chǎn)生約束仇让。因此,當(dāng)更新某一個(gè)變量時(shí)犀变,需要在同一個(gè)原子操作中對(duì)其他變量同時(shí)進(jìn)行更新妹孙。
在上述例子中,雖然set方法是原子操作获枝,但是在set方法無法同時(shí)更新lastNumber和lastFactors蠢正。如果當(dāng)一個(gè)線程執(zhí)行了lastNumber.set()方法還沒執(zhí)行下一個(gè)set方法時(shí),如果此時(shí)有一個(gè)線程訪問service方法省店,那么得到的結(jié)果就與我們所預(yù)期的不一致了嚣崭。
所以笨触,要保持狀態(tài)的一致性,就需要在單個(gè)原子操作中更新所有相關(guān)的狀態(tài)變量雹舀。
三芦劣、加鎖機(jī)制
3.1內(nèi)置鎖
在Java中提供了一種內(nèi)置的鎖機(jī)制來支持原子性:同步代碼塊(Synchronized Block)。同步代碼塊包含兩部分:一個(gè)是作為鎖的對(duì)象引用说榆,一個(gè)作為由這個(gè)鎖保護(hù)的代碼塊虚吟。以關(guān)鍵字synchronized來修飾的方法是一種橫跨整個(gè)方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法調(diào)用所在的對(duì)象(this).靜態(tài)的synchronized方法以Class對(duì)象為作為鎖签财。
每個(gè)Java對(duì)象都可以用做一個(gè)實(shí)現(xiàn)同步的鎖串慰,這些鎖被稱為內(nèi)置鎖(Intrinsic Lock)或是監(jiān)視器鎖(Monitor Lock)。線程在進(jìn)入同步代碼塊之前會(huì)自動(dòng)獲得鎖唱蒸,并且在退出同步代碼塊時(shí)自動(dòng)釋放鎖邦鲫。
Java的內(nèi)置鎖相當(dāng)于一種互斥鎖,最多只有一個(gè)線程能持有這種鎖神汹。當(dāng)線程A嘗試獲取線程B持有的鎖時(shí)庆捺,線程A必須等待或阻塞,知道線程B釋放了該鎖屁魏。如果線程B不釋放鎖滔以,則線程A也將永遠(yuǎn)等下去。任何一個(gè)執(zhí)行同步代碼塊的線程蚁堤,都不可能看到有其他線程正在執(zhí)行由同一個(gè)鎖保護(hù)的同步代碼塊醉者。
下面時(shí)應(yīng)用了內(nèi)置鎖的示例:
public class SynchronizedFactorizer implements Servlet {
private BigInteger lastNumber;
private BigInteger[] lastFactors;
public synchronized void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber)) {
encodeIntoResponse(resp, lastFactors.get());
} else {
BigInteger[] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(resp, factors);
}
}
}
雖然使用synchrnoized關(guān)鍵字保證了結(jié)果的正確性,但是在同一時(shí)刻只有一個(gè)線程可以執(zhí)行service方法披诗,這就導(dǎo)致了服務(wù)的響應(yīng)性非常低,并發(fā)性非常的糟糕立磁,變成了一個(gè)性能問題呈队,而不是線程安全問題。
3.2 重入
當(dāng)某個(gè)線程請(qǐng)求一個(gè)由其他線程持有的鎖是唱歧,發(fā)出請(qǐng)求的線程就會(huì)被阻塞宪摧,但是,由于內(nèi)置鎖是可重入的颅崩,即如果某個(gè)線程試圖獲得一個(gè)已經(jīng)由它自己持有的鎖時(shí)几于,那么這個(gè)請(qǐng)求就會(huì)成功。"重入"意味著獲取鎖的操作粒度是“線程”而不是“調(diào)用”沿后。重入的一種實(shí)現(xiàn)方法就是沿彭,為每一個(gè)鎖關(guān)聯(lián)一個(gè)計(jì)數(shù)值和一個(gè)所有者線程。當(dāng)計(jì)數(shù)值為0時(shí)尖滚,就認(rèn)為這個(gè)鎖是沒有被任何線程持有喉刘。當(dāng)線程請(qǐng)求一個(gè)未被持有的鎖時(shí)瞧柔,JVM記下鎖的持有者,并且將獲取計(jì)數(shù)值置為1睦裳。如果同一個(gè)線程再次獲取這個(gè)鎖造锅,計(jì)數(shù)值將遞增抱慌,而當(dāng)線程退出同步代碼塊時(shí)脚乡,計(jì)數(shù)值會(huì)相應(yīng)的遞減。當(dāng)計(jì)數(shù)值為0時(shí)莱坎,這個(gè)鎖將被釋放蛛蒙。
下面是一個(gè)重入的例子:
public class Widget{
public synchronized void doSomething(){
System.out.println(toString() + ": calling doSomething");
}
}
public class LoggingWidget extends Widget{
public synchronzied void doSomething(){
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
在上述例子中肺素,LoggingWidget繼承了Widget并改寫了父類,并且都是用了synchronized關(guān)鍵字修飾doSomething方法宇驾,如果子類對(duì)象在調(diào)用doSomething方法時(shí)倍靡。如果沒有可重入鎖,那么這段代碼就會(huì)產(chǎn)生死鎖课舍。因?yàn)槊總€(gè)doSomething方法在執(zhí)行前都會(huì)獲得Widget上的鎖塌西,如果內(nèi)置鎖是不可重入的,那么在調(diào)用super.doSomething時(shí)就無法獲得Widget上的鎖筝尾,因?yàn)檫@個(gè)鎖已經(jīng)被持有了捡需,從而線程將永遠(yuǎn)停頓下去,等待一個(gè)永遠(yuǎn)也無法獲得的鎖筹淫。注意:在這里synchronized關(guān)鍵字修飾的是方法體站辉,也就是說它鎖住的是對(duì)象本身(this),所以當(dāng)?shù)谝淮芜M(jìn)入doSomething方法時(shí),鎖住的是LoggingWidget對(duì)象损姜,而在調(diào)用super.doSomething時(shí)饰剥,并沒有新建一個(gè)父類對(duì)象,鎖的對(duì)象還是this.
四摧阅、用鎖來保護(hù)狀態(tài)
對(duì)于可能被多個(gè)線程同時(shí)訪問的可變狀態(tài)變量汰蓉,在訪問它時(shí)都需要持有同一個(gè)鎖,在這種情況下棒卷,我們稱狀態(tài)變量是由這個(gè)鎖保護(hù)的顾孽。對(duì)象的內(nèi)置鎖與其狀態(tài)之間沒有內(nèi)在的關(guān)聯(lián),對(duì)象的域并不一定要通過內(nèi)置鎖類保護(hù)比规。當(dāng)獲取與對(duì)象關(guān)聯(lián)的鎖時(shí)若厚,并不能阻止其他線程訪問該對(duì)象,某個(gè)線程在獲得對(duì)象的鎖之后蜒什,只能阻止其他線程獲得同一個(gè)鎖测秸,每個(gè)對(duì)象都有一個(gè)內(nèi)置鎖。
每個(gè)共享的和可變的變量都應(yīng)該只由一個(gè)鎖來保護(hù)。一種常見的加鎖約定是乞封,將所有可變狀態(tài)都封裝在對(duì)象內(nèi)部做裙,并通過對(duì)象的內(nèi)置鎖對(duì)所有訪問可變狀態(tài)的代碼路勁進(jìn)行同步,使得在該對(duì)象上不會(huì)發(fā)生并發(fā)訪問肃晚。但是锚贱,如果在添加新的方法或代碼路徑時(shí)忘記了使用同步,那么這種加鎖協(xié)議會(huì)很容易被破壞关串。
我們應(yīng)該知道的是拧廊,并非所有數(shù)據(jù)都需要鎖的保護(hù),只有被多個(gè)線程同時(shí)訪問的可變數(shù)據(jù)才需要通過鎖來保護(hù)晋修。當(dāng)某個(gè)變量由鎖來保護(hù)時(shí)吧碾,意味著每次訪問這個(gè)變量時(shí)都需要首先獲得這個(gè)鎖,這樣就確保在同一時(shí)刻只有一個(gè)一個(gè)線程可以訪問這個(gè)變量墓卦。當(dāng)類的不變性條件涉及多個(gè)狀態(tài)變量時(shí)倦春,那么還有另外一個(gè)需求:在不變性條件中的每個(gè)變量都必須由同一個(gè)鎖來保護(hù)。
雖然同步可以避免競態(tài)條件問題落剪,但并不意味著可以在每個(gè)方法聲明時(shí)都是用關(guān)鍵字synchronized.如果將程序中存在過多的同步方法睁本,可能會(huì)導(dǎo)致活躍性問題或性能問題。
我們應(yīng)該盡量將不影響共享狀態(tài)且執(zhí)行時(shí)間較長的操作從同步代碼塊中分離出去忠怖,確保同步代碼塊中盡量只存在原子性的操作呢堰。
在使用鎖時(shí),應(yīng)該清楚代碼塊中實(shí)現(xiàn)的功能凡泣,以及在執(zhí)行該代碼塊時(shí)是否需要很長的時(shí)間枉疼,當(dāng)執(zhí)行時(shí)間較長的計(jì)算或者可能無法快速完成的操作時(shí)(例如,網(wǎng)絡(luò)I/O或控制臺(tái)I/O)鞋拟,一定不要持有鎖!!!