并發(fā)基礎(chǔ)-(一)線程安全性

一隆判、前言

  • 要編寫(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è)程序。

線程安全性.png

二冈闭、什么是線程安全性

  • 在線程安全性的定義中俱尼,最核心的概念就是正確性。正確性的含義是萎攒,某個(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)驹止。

六、活躍性與性能

SynchronizedFactorizer中的不良并發(fā).png

多個(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)拨匆,一定不要持有鎖姆涩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市惭每,隨后出現(xiàn)的幾起案子骨饿,更是在濱河造成了極大的恐慌,老刑警劉巖台腥,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宏赘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡黎侈,警方通過(guò)查閱死者的電腦和手機(jī)察署,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)峻汉,“玉大人贴汪,你說(shuō)我怎么就攤上這事⌒莘停” “怎么了嘶是?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蛛碌。 經(jīng)常有香客問(wèn)我聂喇,道長(zhǎng),這世上最難降的妖魔是什么蔚携? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任希太,我火速辦了婚禮,結(jié)果婚禮上酝蜒,老公的妹妹穿的比我還像新娘誊辉。我一直安慰自己,他們只是感情好亡脑,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布堕澄。 她就那樣靜靜地躺著邀跃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蛙紫。 梳的紋絲不亂的頭發(fā)上拍屑,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音坑傅,去河邊找鬼僵驰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛唁毒,可吹牛的內(nèi)容都是我干的蒜茴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼浆西,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼粉私!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起近零,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤诺核,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后秒赤,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體猪瞬,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年入篮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了陈瘦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡潮售,死狀恐怖痊项,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情酥诽,我是刑警寧澤鞍泉,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站肮帐,受9級(jí)特大地震影響咖驮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜训枢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一托修、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧恒界,春花似錦睦刃、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)际长。三九已至,卻和暖如春兴泥,著一層夾襖步出監(jiān)牢的瞬間工育,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工郁轻, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留翅娶,地道東北人文留。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓好唯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親燥翅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子骑篙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344