一隆判、前言
要編寫(xiě)線程安全的代碼沐飘,其核心在于要對(duì)狀態(tài)訪問(wèn)操作進(jìn)行管理,特別是對(duì)共享(Shared,多線程同時(shí)訪問(wèn))和可變的(Mutable纽帖,變量的值在其生命周期內(nèi)可發(fā)生變化)狀態(tài)的訪問(wèn)。
一個(gè)對(duì)象是否需要是線程安全的提针,取決于它是否被多個(gè)線程訪問(wèn)攒暇。這指的是在程序中訪問(wèn)對(duì)象的方式,而不是對(duì)象要是實(shí)現(xiàn)的功能锦爵。要使得對(duì)象是線程安全的舱殿,需采用同步機(jī)制來(lái)協(xié)同對(duì)象可變狀態(tài)的訪問(wèn)。
Java中的主要同步機(jī)制是關(guān)鍵字synchronized,它提供一個(gè)獨(dú)占的加鎖方式险掀,但“同步”這個(gè)術(shù)語(yǔ)包括volatile類(lèi)型的變量沪袭,顯式鎖(Explicit Lock)以及原子變量。
如果當(dāng)多個(gè)線程訪問(wèn)同一個(gè)可變的狀態(tài)變量時(shí)沒(méi)有使用合適的同步樟氢,那么程序就會(huì)出現(xiàn)錯(cuò)誤冈绊。有三種方式可以修復(fù)這個(gè)問(wèn)題:
不在線程之間共享該狀態(tài)變量。
將狀態(tài)變量修改為不可變的變量埠啃。
在訪問(wèn)狀態(tài)變量時(shí)使用同步焚碌。
- 訪問(wèn)某個(gè)變量的代碼越少,就越容易確保對(duì)變量的所有訪問(wèn)都實(shí)現(xiàn)正確同步霸妹,同時(shí)也更容易找出變量在哪些條件下被訪問(wèn)十电。
當(dāng)設(shè)計(jì)線程安全的類(lèi)時(shí),良好的面向?qū)ο蠹夹g(shù)、不可修改性叹螟,以及明確的不變性規(guī)范都能起到一定的幫助作用鹃骂。
編寫(xiě)并發(fā)應(yīng)用程序時(shí),一種正確的編程方法就是:首先使代碼正確運(yùn)行罢绽,然后再提高代碼的速度畏线,即便如此,最好也只是當(dāng)性能測(cè)試結(jié)果和應(yīng)用需求告訴你必須提高性能良价,以及測(cè)量結(jié)果表明這種優(yōu)化在實(shí)際環(huán)境中確實(shí)能帶來(lái)性能提升時(shí)寝殴,才進(jìn)行優(yōu)化蒿叠。
完全由線程安全類(lèi)構(gòu)成的程序不一定就是線程安全的,而在線程安全類(lèi)中也可以包含非線程安全的類(lèi)蚣常。在任何情況中市咽,只有當(dāng)類(lèi)中僅包含自己的狀態(tài)時(shí),線程安全類(lèi)才是有意義的抵蚊。線程安全性是一個(gè)在代碼上使用的術(shù)語(yǔ)施绎,但它只是與狀態(tài)相關(guān)的,因此只能應(yīng)用于封裝其狀態(tài)的整個(gè)代碼贞绳,這可能是一個(gè)對(duì)象谷醉,也可能是整個(gè)程序。
二冈闭、什么是線程安全性
在線程安全性的定義中俱尼,最核心的概念就是正確性。正確性的含義是萎攒,某個(gè)類(lèi)的行為與其規(guī)范完全一致遇八。在良好的規(guī)范中通常會(huì)定義各種不變性條件(Invariant)來(lái)約束對(duì)象的狀態(tài),以及定義各種后驗(yàn)條件(Postcondition)來(lái)描述對(duì)象操作的結(jié)果躺酒。
我們可以將單線程的正確性近似定義為“所見(jiàn)即所知(we know it when we see it)”押蚤。在對(duì)“正確性”給出了一個(gè)較為清晰的定義后蔑歌,就可以定義線程安全性:當(dāng)多個(gè)線程訪問(wèn)某個(gè)類(lèi)時(shí)羹应,這個(gè)類(lèi)始終都能表現(xiàn)出正確的行為,那么就稱(chēng)這個(gè)類(lèi)為線程安全的次屠。
當(dāng)多個(gè)線程訪問(wèn)某個(gè)類(lèi)時(shí)园匹,不管運(yùn)行的環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同劫灶,這個(gè)類(lèi)都能表現(xiàn)出正確行為裸违,那么就稱(chēng)這個(gè)類(lèi)時(shí)線程安全的。
在線程安全類(lèi)中封裝了必要的同步機(jī)制本昏,因此客戶(hù)端無(wú)須進(jìn)一步采取同步措施供汛。
示例
給出了一個(gè)簡(jiǎn)單的因數(shù)分解Servlet,這個(gè)Servlet從請(qǐng)求中提取數(shù)值涌穆,執(zhí)行因數(shù)分解怔昨,然后將結(jié)果封裝到該Servlet的響應(yīng)中。
@ThreadSafe
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req,ServletResponse resp){
BigIntegert i = exctractFromRequest(req);
BigIntegert [] factors = factor(i);
encodeIntoResponse(resp,factors);
}
}
分析:與大多數(shù)Servlet相同宿稀,StatelessFactorizer 是無(wú)狀態(tài)的:它既不包含任何域趁舀,也不包含任何對(duì)其他類(lèi)中域的引用。計(jì)算過(guò)程中的臨時(shí)狀態(tài)僅存在與線程棧上的局部變量中祝沸,并且只能有正在執(zhí)行的線程訪問(wèn)矮烹。訪問(wèn)StatelessFactorizer 的線程不會(huì)影響到另一個(gè)訪問(wèn)同一個(gè)StatelessFactorizer 的線程的計(jì)算結(jié)果越庇,因?yàn)檫@兩個(gè)線程并沒(méi)有共享狀態(tài),就好像它們都在訪問(wèn)不同的實(shí)例奉狈。由于線程訪問(wèn)無(wú)狀態(tài)對(duì)象的行為并不會(huì)影響其它線程中操作的正確性卤唉,因此無(wú)狀態(tài)對(duì)象是線程安全的。只有當(dāng)Servlet在處理請(qǐng)求時(shí)需要保存一些信息嘹吨,線程安全性才會(huì)成為一個(gè)問(wèn)題搬味。
無(wú)狀態(tài)對(duì)象一定是線程安全的。
三蟀拷、原子性
當(dāng)我們?cè)谝粋€(gè)無(wú)狀態(tài)對(duì)象中增加一個(gè)狀態(tài)會(huì)怎么樣碰纬?
假設(shè)增加一個(gè)“命中計(jì)數(shù)器”來(lái)統(tǒng)計(jì)請(qǐng)求個(gè)數(shù),簡(jiǎn)單的做法是直接加long類(lèi)型的變量问芬,如下(不建議這么做):
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0 ;
public long getCount() { return count; }
public void service(ServletRequest req,ServletResponse resp){
BigIntegert i = exctractFromRequest(req);
BigIntegert [] factors = factor(i);
++count;
encodeIntoResponse(resp,factors);
}
}
盡管它在單線程下可以正常運(yùn)行悦析,但它是線程不安全的。++count看上去是一個(gè)緊湊語(yǔ)法的操作此衅,但它并非原子性强戴,它包含三個(gè)獨(dú)立操作:讀取count的值、將值加1挡鞍、將計(jì)算結(jié)果寫(xiě)入count(讀取-修改-寫(xiě)入 的操作序列)骑歹。它依賴(lài)于之前的狀態(tài)∧ⅲ可能造成如下結(jié)果:兩個(gè)線程沒(méi)同步的情況下道媚,同時(shí)對(duì)計(jì)數(shù)器加1,如果原狀態(tài)為9翘县,則結(jié)果為10最域,實(shí)際上應(yīng)該為11,結(jié)果丟失1锈麸。
在并發(fā)編程中镀脂,由于不恰當(dāng)?shù)膱?zhí)行時(shí)序而出現(xiàn)不正確的結(jié)果是一種非常重要的情況,叫做:競(jìng)態(tài)條件(RaccCondition)忘伞。
3.1 競(jìng)態(tài)條件
當(dāng)某個(gè)計(jì)算的正確性取決于多個(gè)線程的交替執(zhí)行時(shí)序時(shí)薄翅,就會(huì)發(fā)生競(jìng)態(tài)條件。即正確的結(jié)果依靠運(yùn)氣氓奈。最常見(jiàn)的競(jìng)態(tài)條件類(lèi)型就是“先檢查后執(zhí)行(Check-Then-Act)”操作翘魄,即通過(guò)一個(gè)可能失效的觀測(cè)結(jié)果來(lái)決定下一步的動(dòng)作。
本質(zhì)上這種競(jìng)態(tài)條件是基于一種可能失效的觀察結(jié)果來(lái)做出判斷或者執(zhí)行某個(gè)計(jì)算探颈。這種類(lèi)型的競(jìng)態(tài)條件稱(chēng)為“先檢查后執(zhí)行”:首先觀察到某個(gè)條件為真(例如文件X不存在)熟丸,然后根據(jù)這個(gè)觀察結(jié)果開(kāi)始相應(yīng)動(dòng)作(創(chuàng)建文件X),但事實(shí)上伪节,在觀察到這個(gè)結(jié)果及開(kāi)始相應(yīng)操作之間光羞,觀察結(jié)果可能變得無(wú)效(另一個(gè)線程已創(chuàng)建了文件X)绩鸣,從而導(dǎo)致各種問(wèn)題(未預(yù)期的異常、數(shù)據(jù)被覆蓋纱兑、文件被破壞等)呀闻。
3.2 示例:延遲初始化中的競(jìng)態(tài)條件
使用“先檢查過(guò)后執(zhí)行”的一種常見(jiàn)情況就是延遲初始化。延遲初始化的目的是將對(duì)象的初始化操作推遲到實(shí)際被使用時(shí)才進(jìn)行潜慎,同時(shí)要確保只被初始化一次捡多。
例:延遲初始化中的競(jìng)態(tài)條件
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if( instance == null )
instance = new ExpensiveObject();
return instance;
}
}
假定線程A和B同時(shí)執(zhí)行了getInstance。A看到instance為空铐炫,因而創(chuàng)建了一個(gè)新的ExpensiveObject實(shí)例垒手。B同樣需要判斷instance是否為空。此時(shí)instance是否為空要取決于不可預(yù)測(cè)的時(shí)序倒信,包括線程調(diào)度方式科贬,以及A需花多久初始化ExpensiveObject并設(shè)置instance。
競(jìng)態(tài)條件并不總是會(huì)產(chǎn)生錯(cuò)誤鳖悠,還需要某種不恰當(dāng)?shù)膱?zhí)行時(shí)序榜掌。然而,競(jìng)態(tài)條件也可能導(dǎo)致嚴(yán)重的問(wèn)題乘综。
3.3 復(fù)合操作
要避免競(jìng)態(tài)問(wèn)題憎账,就必須在某個(gè)線程修改該變量時(shí),通過(guò)某種方式防止其他線程使用這個(gè)變量卡辰,從而確保其他線程只能在修改操作完成之前或之后讀取和修改狀態(tài)胞皱,而不是在修改狀態(tài)的過(guò)程中。
原子操作是指看政,對(duì)于訪問(wèn)同一個(gè)狀態(tài)的所有操作(包括該操作本身)來(lái)說(shuō)朴恳,這個(gè)操作是一個(gè)以原子方式執(zhí)行的操作抄罕。
為了確保線程安全性允蚣,“先檢查后執(zhí)行”(例如延遲初始化)和“讀取-修改-寫(xiě)入”(例如遞增運(yùn)算)等操作必須是原子的。我們將“先檢查后執(zhí)行”和“讀取-修改-寫(xiě)入”等統(tǒng)稱(chēng)為復(fù)合操作:包含了一組必須以原子方式執(zhí)行的操作以確保線程安全性呆贿。
例:使用AtomicLong類(lèi)型變量來(lái)統(tǒng)計(jì)已處理請(qǐng)求的數(shù)量
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private final ActomicLong count = new ActomicLong(0) ;
public long getCount() { return count.get(); }
public void service(ServletRequest req,ServletResponse resp){
BigIntegert i = exctractFromRequest(req);
BigIntegert [] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp,factors);
}
}
在java.util.concurrent包中包含了一些原子變量類(lèi)嚷兔,用于實(shí)現(xiàn)在數(shù)值和對(duì)象引用上的原子狀態(tài)轉(zhuǎn)換。通過(guò)用AtomicLong來(lái)代替long類(lèi)型的計(jì)數(shù)器做入,能夠確保所有對(duì)計(jì)數(shù)器狀態(tài)的訪問(wèn)操作都是原子的冒晰。
在實(shí)際情況中,應(yīng)盡可能地使用現(xiàn)有的線程安全對(duì)象來(lái)管理類(lèi)的狀態(tài)竟块。與非線程安全的對(duì)象相比壶运,判斷線程安全對(duì)象的可能狀態(tài)及其狀態(tài)轉(zhuǎn)化情況要更為容易,從而也更容易維護(hù)和驗(yàn)證線程安全性浪秘。
四蒋情、加鎖機(jī)制
在線程安全性的定義中要求埠况,多個(gè)線程之間的操作無(wú)論采用何種執(zhí)行時(shí)序或交替方式,都要保證不變性條件不被破壞棵癣。當(dāng)在不變性條件中涉及多個(gè)變量時(shí)辕翰,各個(gè)變量之間并不是彼此獨(dú)立的,而是某個(gè)變量的值會(huì)對(duì)其它變量的值產(chǎn)生約束狈谊。因此喜命,當(dāng)更新某一個(gè)變量時(shí),需要在同一個(gè)原子操作中對(duì)其他變量同時(shí)進(jìn)行更新河劝。
如果只修改了其中一個(gè)變量壁榕,那么在這兩次修改操作之間,其他線程將發(fā)現(xiàn)不變性條件被破壞了赎瞎。同樣我們也不能保證會(huì)同時(shí)獲取兩個(gè)值:在線程A獲取這兩個(gè)值的過(guò)程中护桦,線程B可能修改了它們,這樣線程A也會(huì)發(fā)現(xiàn)不變性條件被破壞了煎娇。
要保持狀態(tài)的一致性二庵,就需要在單個(gè)原子操作中更新所有相關(guān)的狀態(tài)變量。
4.1 內(nèi)置鎖
Java提供了一種內(nèi)置的鎖機(jī)制來(lái)支持原子性:同步代碼塊(Synchronized Block)缓呛。包括兩部分:一個(gè)作為鎖的對(duì)象引用催享,一個(gè)作為由這個(gè)鎖保護(hù)的代碼塊。以關(guān)鍵字synchronized來(lái)修飾的方法是橫跨整個(gè)方法體的同步代碼塊哟绊。其中該同步塊的鎖就是方法調(diào)用所在的對(duì)象因妙。靜態(tài)的synchronized方法以Class對(duì)象作為鎖。
synchronized(lock){
//訪問(wèn)或修改由鎖保護(hù)的共享狀態(tài)
}
每個(gè)Java對(duì)象都可以用做一個(gè)實(shí)現(xiàn)同步的鎖票髓,這些鎖被稱(chēng)為內(nèi)置鎖(Intrinsic Lock)或監(jiān)視器鎖(Monitor Lock)攀涵。線程在進(jìn)入同步代碼塊之前會(huì)自動(dòng)獲得鎖,退出時(shí)自動(dòng)釋放鎖洽沟。獲得內(nèi)置鎖的唯一途徑就是進(jìn)入由這個(gè)鎖保護(hù)的同步代碼塊或方法以故。
Java的內(nèi)置鎖相當(dāng)于一種互斥體(互斥鎖),這意味著最多只有一個(gè)線程能保持這個(gè)鎖裆操。
由于每次只能由一個(gè)線程執(zhí)行內(nèi)置鎖保護(hù)的代碼塊怒详,因此,由這個(gè)鎖保護(hù)的同步代碼塊會(huì)以原子方式執(zhí)行踪区,多個(gè)線程在執(zhí)行該代碼塊時(shí)也不會(huì)互相干擾昆烁。并發(fā)環(huán)境中的原子性與事務(wù)應(yīng)用程序中的原子性有著相同的含義--一組語(yǔ)句作為一個(gè)不可分割的單元被執(zhí)行。
4.2 重入
當(dāng)某個(gè)線程請(qǐng)求一個(gè)由其他線程持有的鎖時(shí)缎岗,發(fā)出請(qǐng)求的線程就會(huì)阻塞静尼。由于內(nèi)置鎖是可重入的,因此若某個(gè)線程視圖獲得一個(gè)已經(jīng)由它自己持有的鎖,那么這個(gè)請(qǐng)求就會(huì)成功鼠渺∥显“重入”意味著獲取鎖的操作的粒度是“線程”,而不是“調(diào)用”系冗。重入的一種實(shí)現(xiàn)方法是奕扣,為每個(gè)鎖關(guān)聯(lián)一個(gè)獲取計(jì)數(shù)值和一個(gè)所有者線程。當(dāng)計(jì)數(shù)值為0時(shí)掌敬,這個(gè)鎖就被認(rèn)為是沒(méi)有被任何線程持有惯豆。當(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è)鎖將會(huì)釋放揭厚。
重入進(jìn)一步提升了加鎖行為的封裝性,因此簡(jiǎn)化了面向?qū)ο蟛l(fā)代碼的開(kāi)發(fā)扶供。
五筛圆、用鎖來(lái)保護(hù)狀態(tài)
由于鎖能使其保護(hù)的代碼路徑以串行形式來(lái)訪問(wèn),因此可通過(guò)鎖來(lái)構(gòu)造一些協(xié)議以實(shí)現(xiàn)對(duì)共享狀態(tài)的獨(dú)占訪問(wèn)椿浓。只要始終遵循這些協(xié)議太援,就能確保狀態(tài)的一致性。
如果在符合操作的執(zhí)行過(guò)程中持有一個(gè)鎖扳碍。如果會(huì)使復(fù)合操作成為原子操作提岔。然而,僅僅將復(fù)合操作封裝到一個(gè)同步代碼塊中是不夠的笋敞。如果用同步來(lái)協(xié)調(diào)對(duì)某個(gè)變量的訪問(wèn)碱蒙,那么在訪問(wèn)這個(gè)變量的所有位置上都需要使用同步。而且液样,當(dāng)使用鎖來(lái)協(xié)調(diào)對(duì)某個(gè)變量的訪問(wèn)時(shí)振亮,在訪問(wèn)變量的所有位置上都要使用同一個(gè)鎖巧还。
由于可能被多個(gè)線程同時(shí)訪問(wèn)的可變狀態(tài)變量鞭莽,在訪問(wèn)它時(shí)都需要持有同一個(gè)鎖,在這種情況下麸祷,我們稱(chēng)狀態(tài)變量是由這個(gè)鎖保護(hù)的澎怒。
對(duì)象的內(nèi)置鎖與其狀態(tài)之間沒(méi)有內(nèi)在的關(guān)聯(lián)。對(duì)象的域并不一定要通過(guò)內(nèi)置鎖來(lái)保護(hù)。當(dāng)獲取與對(duì)象關(guān)聯(lián)的鎖時(shí)喷面,并不能阻止其他線程訪問(wèn)該對(duì)象星瘾,某個(gè)線程在獲得對(duì)象的鎖之后,只能阻止其他線程獲得同一個(gè)鎖惧辈。之所以每個(gè)對(duì)象都有一個(gè)內(nèi)置鎖琳状,只是為了免去顯示地創(chuàng)建鎖對(duì)象。你需要自行構(gòu)造加鎖協(xié)議或者同步策略來(lái)實(shí)現(xiàn)對(duì)共享狀態(tài)的安全訪問(wèn)盒齿,并且在程序中自始至終地使用它們念逞。
每個(gè)共享的和可變的變量都應(yīng)該只由一個(gè)鎖來(lái)保護(hù),從而使維護(hù)人員知道是哪一個(gè)鎖边翁。
一種常見(jiàn)的加鎖約定是翎承,將所有的可變狀態(tài)都封裝在對(duì)象內(nèi)部,并通過(guò)對(duì)象的內(nèi)置鎖對(duì)所有訪問(wèn)可變狀態(tài)的路徑進(jìn)行同步符匾,使得在該對(duì)象上不會(huì)發(fā)生并發(fā)訪問(wèn)叨咖。如果添加新的方法或代碼路徑時(shí)忘記了使用同步,那么這種加鎖協(xié)議會(huì)很容易被破壞啊胶。
并非所有數(shù)據(jù)都需要鎖的保護(hù)甸各,只有被多個(gè)線程同時(shí)訪問(wèn)的可變數(shù)據(jù)才需要通過(guò)鎖來(lái)保護(hù)。
當(dāng)某個(gè)變量由說(shuō)來(lái)保護(hù)時(shí)焰坪,意味著在每次訪問(wèn)這個(gè)變量時(shí)都需要首先獲得鎖痴晦,這樣就確保在同一個(gè)時(shí)刻只有一個(gè)線程可以訪問(wèn)這個(gè)變量。當(dāng)類(lèi)的不可變性條件涉及多個(gè)狀態(tài)變量時(shí)琳彩,那么還有另外一個(gè)需求:在不可變性條件中的每個(gè)變量都必須由同一個(gè)鎖來(lái)保護(hù)誊酌。因此可以在按個(gè)原子操作中訪問(wèn)或更新這些變量,從而確保不變性條件不被破壞露乏。
對(duì)于每個(gè)包含多個(gè)變量的不變性條件碧浊,其中涉及的所有變量都需要由同一個(gè)鎖來(lái)保護(hù)。
雖然synchronized方法可以確保單個(gè)操作的原子性瘟仿,但如果要把多個(gè)操作合并為一個(gè)復(fù)合操作箱锐,還需要額外的加鎖機(jī)制。此外劳较,將每個(gè)方法都作為同步方法還可能導(dǎo)致活躍性問(wèn)題(Livveness)或性能問(wèn)題(Performance)驹止。
六、活躍性與性能
多個(gè)請(qǐng)求同時(shí)到達(dá)因數(shù)分解時(shí):這些請(qǐng)求將排隊(duì)等待處理观蜗。我們將這種Web應(yīng)用程序稱(chēng)之為不良并發(fā)(Poor Concurrency)應(yīng)用程序:可同時(shí)調(diào)用的數(shù)量臊恋,不僅受到可用處理資源的限制,還受到應(yīng)用程序本身結(jié)構(gòu)的限制墓捻。幸運(yùn)的是抖仅,通過(guò)縮小同步代碼塊的作用范圍,我們很容易做到既確保Servlet的并發(fā)性,同時(shí)又維護(hù)線程安全性撤卢。要確保同步代碼塊不要過(guò)小环凿,并且不要將本應(yīng)是原子的操作拆分到多個(gè)同步代碼塊中。應(yīng)該盡量將不影響共享狀態(tài)且執(zhí)行時(shí)間較長(zhǎng)的操作從同步代碼塊中分離出去放吩,從而在這些操作的執(zhí)行過(guò)程中智听,其他線程可以訪問(wèn)共享狀態(tài)。
@ThreadSafe
public class CachedFactorizer implements Servlet {
@GuardedBy("this") priavte BigInteger lastNumber;
@GuardedBy("this") priavte BigInteger[] lastFactors;
@GuardedBy("this") priavte long hits;
@GuardedBy("this") priavte long cacheHits;
public synchronized long getHits(){ return hits; }
public synchronized double getCacheHitRatio() {
return (double) cacheHits / (double) hits;
}
public void service(ServletRequest req, ServletResponse resp ){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized (this){
++this;
if ( i.equals(lastNumber) ) {
++cacheHits;
factors = lastFactors.clone();
}
}
if ( factors == null ) {
factors = factor(i);
synchronized (this) {
lastNumber = i ;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp , factors);
}
}
代碼修改為使用兩個(gè)獨(dú)立的同步代碼塊渡紫,每個(gè)同步代碼塊都只包含一小段代碼瞭稼。其中一個(gè)同步代碼塊負(fù)責(zé)保護(hù)判斷是否只需返回緩存結(jié)果的“先檢查后執(zhí)行”操作序列,另一個(gè)同步代碼塊則負(fù)責(zé)確保對(duì)緩存的數(shù)值和因素分解結(jié)果進(jìn)行同步更新腻惠。此外還重新引入“命中計(jì)數(shù)器”环肘,添加一個(gè)“緩存命中計(jì)數(shù)器”,并在第一個(gè)同步代碼塊中更新這兩個(gè)變量集灌。由于兩個(gè)計(jì)數(shù)器也共享可變狀態(tài)的一部分悔雹,因此必須在所有所有訪問(wèn)他們的位置上都使用同步。位于同步代碼塊之外的代碼將以獨(dú)占方式來(lái)訪問(wèn)局部(位于棧上的)變量欣喧,這些變量不會(huì)在多個(gè)線程間共享腌零,因此不需同步。
對(duì)在單個(gè)變量上實(shí)現(xiàn)原子操作來(lái)說(shuō)唆阿,原子變量是很用的益涧,但由于我們已經(jīng)使用了同步代碼塊來(lái)構(gòu)造原子操作,而使用兩種不同的同步機(jī)制不僅不會(huì)帶來(lái)混亂驯鳖,也不會(huì)再性能或安全性上帶來(lái)任何好處闲询,因此在這里不使用原子變量。
重新構(gòu)造后的CachedFactorizer實(shí)現(xiàn)了在簡(jiǎn)單性(對(duì)整個(gè)方法進(jìn)行同步)與并發(fā)性(對(duì)盡可能短的代碼路徑進(jìn)行同步)之間的平衡浅辙。在獲取與釋放鎖等操作上都需要一定的開(kāi)銷(xiāo)扭弧,因此如果將同步代碼塊分解得過(guò)細(xì),那么通常并不好记舆,盡管不會(huì)破壞原子性鸽捻。當(dāng)訪問(wèn)狀態(tài)變量或者在復(fù)合操作的執(zhí)行期間,CachedFactorizer需要持有鎖泽腮,但在執(zhí)行時(shí)間較長(zhǎng)的因數(shù)分解運(yùn)算之前要釋放鎖御蒲。這樣既確保了線程安全性,也不會(huì)過(guò)多地影響并發(fā)性诊赊,而且在每個(gè)同步代碼塊中的代碼路徑都“足夠短”厚满。
要判斷同步代碼塊的合理大小,需要在各種設(shè)計(jì)需要之間進(jìn)行權(quán)衡豪筝,包括安全性(這個(gè)需求必須得到滿(mǎn)足)痰滋、簡(jiǎn)單性和性能摘能。有時(shí)候续崖,在簡(jiǎn)單性與性能之間會(huì)發(fā)生沖突敲街,但在CachedFactorier中已經(jīng)說(shuō)明了,在二者之間通常能找到某種合理的平衡严望。
通常多艇,在簡(jiǎn)單性與性能之間存在著互相制約因素。當(dāng)實(shí)現(xiàn)某個(gè)同步策略時(shí)像吻,一定不要盲目地為了性能而犧牲簡(jiǎn)單性(這可能會(huì)破壞安全性)峻黍。
當(dāng)執(zhí)行時(shí)間較長(zhǎng)的計(jì)算或者可能無(wú)法快速完成的操作時(shí)(例如,網(wǎng)絡(luò)I/O或控制臺(tái)I/O)拨匆,一定不要持有鎖姆涩。