<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    蘋果的成長(zhǎng)日記

    我還是個(gè)青蘋果呀!

      BlogJava :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
      57 隨筆 :: 0 文章 :: 74 評(píng)論 :: 0 Trackbacks
    輕松使用線程: 同步不是敵人
     
    內(nèi)容:
    synchronized 真正意味著什么?
    使用一條好的運(yùn)行路線
    同步的代價(jià)有多大?
    不要爭(zhēng)用
    什么時(shí)候需要同步?
    如果情況不確定,考慮使用同步包裝
    結(jié)論
    參考資料
    關(guān)于作者
    對(duì)本文的評(píng)價(jià)
    相關(guān)內(nèi)容:
    編寫多線程的 Java 應(yīng)用程序
    用固定的,循環(huán)的順序獲取多個(gè)鎖定以避免死鎖
    更多的 dW Java 參考資料
    訂閱:
    developerWorks 時(shí)事通訊
    我們什么時(shí)候需要同步,而同步的代價(jià)到底有多大?

    Brian Goetz
    軟件顧問, Quiotix
    2001 年 7 月 04 日

    與許多其它的編程語(yǔ)言不同,Java語(yǔ)言規(guī)范包括對(duì)線程和并發(fā)的明確支持。語(yǔ)言本身支持并發(fā),這使得指定和管理共享數(shù)據(jù)的約束以及跨線程操作的計(jì)時(shí)變得更簡(jiǎn)單,但是這沒有使得并發(fā)編程的復(fù)雜性更易于理解。這個(gè)三部分的系列文章的目的在于幫助程序員理解用Java 語(yǔ)言進(jìn)行多線程編程的一些主要問題,特別是線程安全對(duì) Java程序性能的影響。

    請(qǐng)點(diǎn)擊文章頂部或底部的 討論進(jìn)入由 Brian Goetz 主持的 “Java線程:技巧、竅門和技術(shù)”討論論壇,與本文作者和其他讀者交流您對(duì)本文或整個(gè)多線程的想法。注意該論壇討論的是使用多線程時(shí)遇到的所有問題,而并不限于本文的內(nèi)容。

    大多數(shù)編程語(yǔ)言的語(yǔ)言規(guī)范都不會(huì)談到線程和并發(fā)的問題;因?yàn)橐恢币詠?lái),這些問題都是留給平臺(tái)或操作系統(tǒng)去詳細(xì)說明的。但是,Java 語(yǔ)言規(guī)范(JLS)卻明確包括一個(gè)線程模型,并提供了一些語(yǔ)言元素供開發(fā)人員使用以保證他們程序的線程安全。

    對(duì)線程的明確支持有利也有弊。它使得我們?cè)趯懗绦驎r(shí)更容易利用線程的功能和便利,但同時(shí)也意味著我們不得不注意所寫類的線程安全,因?yàn)槿魏晤惗己苡锌赡鼙挥迷谝粋€(gè)多線程的環(huán)境內(nèi)。

    許多用戶第一次發(fā)現(xiàn)他們不得不去理解線程的概念的時(shí)候,并不是因?yàn)樗麄冊(cè)趯憚?chuàng)建和管理線程的程序,而是因?yàn)樗麄冋谟靡粋€(gè)本身是多線程的工具或框架。任何用過 Swing GUI 框架或?qū)戇^小服務(wù)程序或 JSP 頁(yè)的開發(fā)人員(不管有沒有意識(shí)到)都曾經(jīng)被線程的復(fù)雜性困擾過。

    Java 設(shè)計(jì)師是想創(chuàng)建一種語(yǔ)言,使之能夠很好地運(yùn)行在現(xiàn)代的硬件,包括多處理器系統(tǒng)上。要達(dá)到這一目的,管理線程間協(xié)調(diào)的工作主要推給了軟件開發(fā)人員;程序員必須指定線程間共享數(shù)據(jù)的位置。在 Java 程序中,用來(lái)管理線程間協(xié)調(diào)工作的主要工具是 synchronized 關(guān)鍵字。在缺少同步的情況下,JVM 可以很自由地對(duì)不同線程內(nèi)執(zhí)行的操作進(jìn)行計(jì)時(shí)和排序。在大部分情況下,這正是我們想要的,因?yàn)檫@樣可以提高性能,但它也給程序員帶來(lái)了額外的負(fù)擔(dān),他們不得不自己識(shí)別什么時(shí)候這種性能的提高會(huì)危及程序的正確性。

    synchronized 真正意味著什么?
    大部分 Java 程序員對(duì)同步的塊或方法的理解是完全根據(jù)使用互斥(互斥信號(hào)量)或定義一個(gè)臨界段(一個(gè)必須原子性地執(zhí)行的代碼塊)。雖然 synchronized 的語(yǔ)義中確實(shí)包括互斥和原子性,但在管程進(jìn)入之前和在管程退出之后發(fā)生的事情要復(fù)雜得多。

    synchronized 的語(yǔ)義確實(shí)保證了一次只有一個(gè)線程可以訪問被保護(hù)的區(qū)段,但同時(shí)還包括同步線程在主存內(nèi)互相作用的規(guī)則。理解 Java 內(nèi)存模型(JMM)的一個(gè)好方法就是把各個(gè)線程想像成運(yùn)行在相互分離的處理器上,所有的處理器存取同一塊主存空間,每個(gè)處理器有自己的緩存,但這些緩存可能并不總和主存同步。在缺少同步的情況下,JMM 會(huì)允許兩個(gè)線程在同一個(gè)內(nèi)存地址上看到不同的值。而當(dāng)用一個(gè)管程(鎖)進(jìn)行同步的時(shí)候,一旦申請(qǐng)加了鎖,JMM 就會(huì)馬上要求該緩存失效,然后在它被釋放前對(duì)它進(jìn)行刷新(把修改過的內(nèi)存位置寫回主存)。不難看出為什么同步會(huì)對(duì)程序的性能影響這么大;頻繁地刷新緩存代價(jià)會(huì)很大。

    使用一條好的運(yùn)行路線
    如果同步不適當(dāng),后果是很嚴(yán)重的:會(huì)造成數(shù)據(jù)混亂和爭(zhēng)用情況,導(dǎo)致程序崩潰,產(chǎn)生不正確的結(jié)果,或者是不可預(yù)計(jì)的運(yùn)行。更糟的是,這些情況可能很少發(fā)生且具有偶然性(使得問題很難被監(jiān)測(cè)和重現(xiàn))。如果測(cè)試環(huán)境和開發(fā)環(huán)境有很大的不同,無(wú)論是配置的不同,還是負(fù)荷的不同,都有可能使得這些問題在測(cè)試環(huán)境中根本不出現(xiàn),從而得出錯(cuò)誤的結(jié)論:我們的程序是正確的,而事實(shí)上這些問題只是還沒出現(xiàn)而已。

    爭(zhēng)用情況定義
    爭(zhēng)用情況是一種特定的情況:兩個(gè)或更多的線程或進(jìn)程讀或?qū)懸恍┕蚕頂?shù)據(jù),而最終結(jié)果取決于這些線程是如何被調(diào)度計(jì)時(shí)的。爭(zhēng)用情況可能會(huì)導(dǎo)致不可預(yù)見的結(jié)果和隱蔽的程序錯(cuò)誤。

    另一方面,不當(dāng)或過度地使用同步會(huì)導(dǎo)致其它問題,比如性能很差和死鎖。當(dāng)然,性能差雖然不如數(shù)據(jù)混亂那么嚴(yán)重,但也是一個(gè)嚴(yán)重的問題,因此同樣不可忽視。編寫優(yōu)秀的多線程程序需要使用好的運(yùn)行路線,足夠的同步可以使您的數(shù)據(jù)不發(fā)生混亂,但不需要濫用到去承擔(dān)死鎖或不必要地削弱程序性能的風(fēng)險(xiǎn)。

    同步的代價(jià)有多大?
    由于包括緩存刷新和設(shè)置失效的過程,Java 語(yǔ)言中的同步塊通常比許多平臺(tái)提供的臨界段設(shè)備代價(jià)更大,這些臨界段通常是用一個(gè)原子性的“test and set bit”機(jī)器指令實(shí)現(xiàn)的。即使一個(gè)程序只包括一個(gè)在單一處理器上運(yùn)行的單線程,一個(gè)同步的方法調(diào)用仍要比非同步的方法調(diào)用慢。如果同步時(shí)還發(fā)生鎖定爭(zhēng)用,那么性能上付出的代價(jià)會(huì)大得多,因?yàn)闀?huì)需要幾個(gè)線程切換和系統(tǒng)調(diào)用。

    幸運(yùn)的是,隨著每一版的 JVM 的不斷改進(jìn),既提高了 Java 程序的總體性能,同時(shí)也相對(duì)減少了同步的代價(jià),并且將來(lái)還可能會(huì)有進(jìn)一步的改進(jìn)。此外,同步的性能代價(jià)經(jīng)常是被夸大的。一個(gè)著名的資料來(lái)源就曾經(jīng)引證說一個(gè)同步的方法調(diào)用比一個(gè)非同步的方法調(diào)用慢 50 倍。雖然這句話有可能是真的,但也會(huì)產(chǎn)生誤導(dǎo),而且已經(jīng)導(dǎo)致了許多開發(fā)人員即使在需要的時(shí)候也避免使用同步。

    嚴(yán)格依照百分比計(jì)算同步的性能損失并沒有多大意義,因?yàn)橐粋€(gè)無(wú)爭(zhēng)用的同步給一個(gè)塊或方法帶來(lái)的是固定的性能損失。而這一固定的延遲帶來(lái)的性能損失百分比取決于在該同步塊內(nèi)做了多少工作。對(duì)一個(gè) 方法的同步調(diào)用可能要比對(duì)一個(gè)空方法的非同步調(diào)用慢 20 倍,但我們多長(zhǎng)時(shí)間才調(diào)用一次空方法呢?當(dāng)我們用更有代表性的小方法來(lái)衡量同步損失時(shí),百分?jǐn)?shù)很快就下降到可以容忍的范圍之內(nèi)。

    表 1 把一些這種數(shù)據(jù)放在一起來(lái)看。它列舉了一些不同的實(shí)例,不同的平臺(tái)和不同的 JVM 下一個(gè)同步的方法調(diào)用相對(duì)于一個(gè)非同步的方法調(diào)用的損失。在每一個(gè)實(shí)例下,我運(yùn)行一個(gè)簡(jiǎn)單的程序,測(cè)定循環(huán)調(diào)用一個(gè)方法 10,000,000 次所需的運(yùn)行時(shí)間,我調(diào)用了同步和非同步兩個(gè)版本,并比較了結(jié)果。表格中的數(shù)據(jù)是同步版本的運(yùn)行時(shí)間相對(duì)于非同步版本的運(yùn)行時(shí)間的比率;它顯示了同步的性能損失。每次運(yùn)行調(diào)用的都是清單 1 中的簡(jiǎn)單方法之一。

    表格 1 中顯示了同步方法調(diào)用相對(duì)于非同步方法調(diào)用的相對(duì)性能;為了用絕對(duì)的標(biāo)準(zhǔn)測(cè)定性能損失,必須考慮到 JVM 速度提高的因素,這并沒有在數(shù)據(jù)中體現(xiàn)出來(lái)。在大多數(shù)測(cè)試中,每個(gè) JVM 的更高版本都會(huì)使 JVM 的總體性能得到很大提高,很有可能 1.4 版的 Java 虛擬機(jī)發(fā)行的時(shí)候,它的性能還會(huì)有進(jìn)一步的提高。

    表 1. 無(wú)爭(zhēng)用同步的性能損失
    JDK staticEmpty empty fetch hashmapGet singleton create
    Linux / JDK 1.1 9.2 2.4 2.5 n/a 2.0 1.42
    Linux / IBM Java SDK 1.1 33.9 18.4 14.1 n/a 6.9 1.2
    Linux / JDK 1.2 2.5 2.2 2.2 1.64 2.2 1.4
    Linux / JDK 1.3 (no JIT) 2.52 2.58 2.02 1.44 1.4 1.1
    Linux / JDK 1.3 -server 28.9 21.0 39.0 1.87 9.0 2.3
    Linux / JDK 1.3 -client 21.2 4.2 4.3 1.7 5.2 2.1
    Linux / IBM Java SDK 1.3 8.2 33.4 33.4 1.7 20.7 35.3
    Linux / gcj 3.0 2.1 3.6 3.3 1.2 2.4 2.1
    Solaris / JDK 1.1 38.6 20.1 12.8 n/a 11.8 2.1
    Solaris / JDK 1.2 39.2 8.6 5.0 1.4 3.1 3.1
    Solaris / JDK 1.3 (no JIT) 2.0 1.8 1.8 1.0 1.2 1.1
    Solaris / JDK 1.3 -client 19.8 1.5 1.1 1.3 2.1 1.7
    Solaris / JDK 1.3 -server 1.8 2.3 53.0 1.3 4.2 3.2

    清單 1. 基準(zhǔn)測(cè)試中用到的簡(jiǎn)單方法
     public static void staticEmpty() {  }
    
      public void empty() {  }
    
      public Object fetch() { return field; }
    
      public Object singleton() {
        if (singletonField == null)
          singletonField = new Object();
        return singletonField;
      }
    
      public Object hashmapGet() {
        return hashMap.get("this");
      }
    
      public Object create() { 
        return new Object();
      }
    

    這些小基準(zhǔn)測(cè)試也闡明了存在動(dòng)態(tài)編譯器的情況下解釋性能結(jié)果所面臨的挑戰(zhàn)。對(duì)于 1.3 JDK 在有和沒有 JIT 時(shí),數(shù)字上的巨大差異需要給出一些解釋。對(duì)那些非常簡(jiǎn)單的方法( emptyfetch ),基準(zhǔn)測(cè)試的本質(zhì)(它只是執(zhí)行一個(gè)幾乎什么也不做的緊湊的循環(huán))使得 JIT 可以動(dòng)態(tài)地編譯整個(gè)循環(huán),把運(yùn)行時(shí)間壓縮到幾乎沒有的地步。但在一個(gè)實(shí)際的程序中,JIT 能否這樣做就要取決于很多因素了,所以,無(wú) JIT 的計(jì)時(shí)數(shù)據(jù)可能在做公平對(duì)比時(shí)更有用一些。在任何情況下,對(duì)于更充實(shí)的方法( createhashmapGet ),JIT 就不能象對(duì)更簡(jiǎn)單些的方法那樣使非同步的情況得到巨大的改進(jìn)。另外,從數(shù)據(jù)中看不出 JVM 是否能夠?qū)y(cè)試的重要部分進(jìn)行優(yōu)化。同樣,在可比較的 IBM 和 Sun JDK 之間的差異反映了 IBM Java SDK 可以更大程度地優(yōu)化非同步的循環(huán),而不是同步版本代價(jià)更高。這在純計(jì)時(shí)數(shù)據(jù)中可以明顯地看出(這里不提供)。

    從這些數(shù)字中我們可以得出以下結(jié)論:對(duì)非爭(zhēng)用同步而言,雖然存在性能損失,但在運(yùn)行許多不是特別微小的方法時(shí),損失可以降到一個(gè)合理的水平;大多數(shù)情況下?lián)p失大概在 10% 到 200% 之間(這是一個(gè)相對(duì)較小的數(shù)目)。所以,雖然同步每個(gè)方法是不明智的(這也會(huì)增加死鎖的可能性),但我們也不需要這么害怕同步。這里使用的簡(jiǎn)單測(cè)試是說明一個(gè)無(wú)爭(zhēng)用同步的代價(jià)要比創(chuàng)建一個(gè)對(duì)象或查找一個(gè) HashMap 的代價(jià)小。

    由于早期的書籍和文章暗示了無(wú)爭(zhēng)用同步要付出巨大的性能代價(jià),許多程序員就竭盡全力避免同步。這種恐懼導(dǎo)致了許多有問題的技術(shù)出現(xiàn),比如說 double-checked locking(DCL)。許多關(guān)于 Java 編程的書和文章都推薦 DCL,它看上去真是避免不必要的同步的一種聰明的方法,但實(shí)際上它根本沒有用,應(yīng)該避免使用它。DCL 無(wú)效的原因很復(fù)雜,已超出了本文討論的范圍(要深入了解,請(qǐng)參閱 參考資料里的鏈接)。

    不要爭(zhēng)用
    假設(shè)同步使用正確,若線程真正參與爭(zhēng)用加鎖,您也能感受到同步對(duì)實(shí)際性能的影響。并且無(wú)爭(zhēng)用同步和爭(zhēng)用同步間的性能損失差別很大;一個(gè)簡(jiǎn)單的測(cè)試程序指出爭(zhēng)用同步比無(wú)爭(zhēng)用同步慢 50 倍。把這一事實(shí)和我們上面抽取的觀察數(shù)據(jù)結(jié)合在一起,可以看出使用一個(gè)爭(zhēng)用同步的代價(jià)至少相當(dāng)于創(chuàng)建 50 個(gè)對(duì)象。

    所以,在調(diào)試應(yīng)用程序中同步的使用時(shí),我們應(yīng)該努力減少實(shí)際爭(zhēng)用的數(shù)目,而根本不是簡(jiǎn)單地試圖避免使用同步。這個(gè)系列的第 2 部分將把重點(diǎn)放在減少爭(zhēng)用的技術(shù)上,包括減小鎖的粒度、減小同步塊的大小以及減小線程間共享數(shù)據(jù)的數(shù)量。

    什么時(shí)候需要同步?
    要使您的程序線程安全,首先必須確定哪些數(shù)據(jù)將在線程間共享。如果正在寫的數(shù)據(jù)以后可能被另一個(gè)線程讀到,或者正在讀的數(shù)據(jù)可能已經(jīng)被另一個(gè)線程寫過了,那么這些數(shù)據(jù)就是共享數(shù)據(jù),必須進(jìn)行同步存取。有些程序員可能會(huì)驚訝地發(fā)現(xiàn),這些規(guī)則在簡(jiǎn)單地檢查一個(gè)共享引用是否非空的時(shí)候也用得上。

    許多人會(huì)發(fā)現(xiàn)這些定義驚人地嚴(yán)格。有一種普遍的觀點(diǎn)是,如果只是要讀一個(gè)對(duì)象的字段,不需要請(qǐng)求加鎖,尤其是在 JLS 保證了 32 位讀操作的原子性的情況下,它更是如此。但不幸的是,這個(gè)觀點(diǎn)是錯(cuò)誤的。除非所指的字段被聲明為 volatile ,否則 JMM 不會(huì)要求下面的平臺(tái)提供處理器間的緩存一致性和順序連貫性,所以很有可能,在某些平臺(tái)上,沒有同步就會(huì)讀到陳舊的數(shù)據(jù)。有關(guān)更詳細(xì)的信息,請(qǐng)參閱 參考資料

    在確定了要共享的數(shù)據(jù)之后,還要確定要如何保護(hù)那些數(shù)據(jù)。在簡(jiǎn)單情況下,只需把它們聲明為 volatile 即可保護(hù)數(shù)據(jù)字段;在其它情況下,必須在讀或?qū)懝蚕頂?shù)據(jù)前請(qǐng)求加鎖,一個(gè)很好的經(jīng)驗(yàn)是明確指出使用什么鎖來(lái)保護(hù)給定的字段或?qū)ο螅⒃谀愕拇a里把它記錄下來(lái)。

    還有一點(diǎn)值得注意的是,簡(jiǎn)單地同步存取器方法(或聲明下層的字段為 volatile )可能并不足以保護(hù)一個(gè)共享字段。可以考慮下面的示例:

     ...
      private int foo;
      public synchronized int getFoo() { return foo; } 
      public synchronized void setFoo(int f) { foo = f; }
    

    如果一個(gè)調(diào)用者想要增加 foo 屬性值,以下完成該功能的代碼就不是線程安全的:

     ...
      setFoo(getFoo() + 1);
    

    如果兩個(gè)線程試圖同時(shí)增加 foo 屬性值,結(jié)果可能是 foo 的值增加了 1 或 2,這由計(jì)時(shí)決定。調(diào)用者將需要同步一個(gè)鎖,才能防止這種爭(zhēng)用情況;一個(gè)好方法是在 JavaDoc 類中指定同步哪個(gè)鎖,這樣類的調(diào)用者就不需要自己猜了。

    以上情況是一個(gè)很好的示例,說明我們應(yīng)該注意多層次粒度的數(shù)據(jù)完整性;同步存取器方法確保調(diào)用者能夠存取到一致的和最近版本的屬性值,但如果希望屬性的將來(lái)值與當(dāng)前值一致,或多個(gè)屬性間相互一致,我們就必須同步復(fù)合操作 ― 可能是在一個(gè)粗粒度的鎖上。

    如果情況不確定,考慮使用同步包裝
    有時(shí),在寫一個(gè)類的時(shí)候,我們并不知道它是否要用在一個(gè)共享環(huán)境里。我們希望我們的類是線程安全的,但我們又不希望給一個(gè)總是在單線程環(huán)境內(nèi)使用的類加上同步的負(fù)擔(dān),而且我們可能也不知道使用這個(gè)類時(shí)合適的鎖粒度是多大。幸運(yùn)的是,通過提供同步包裝,我們可以同時(shí)達(dá)到以上兩個(gè)目的。Collections 類就是這種技術(shù)的一個(gè)很好的示例;它們是非同步的,但在框架中定義的每個(gè)接口都有一個(gè)同步包裝(例如, Collections.synchronizedMap() ),它用一個(gè)同步的版本來(lái)包裝每個(gè)方法。

    結(jié)論
    雖然 JLS 給了我們可以使我們的程序線程安全的工具,但線程安全也不是天上掉下來(lái)的餡餅。使用同步會(huì)蒙受性能損失,而同步使用不當(dāng)又會(huì)使我們承擔(dān)數(shù)據(jù)混亂、結(jié)果不一致或死鎖的風(fēng)險(xiǎn)。幸運(yùn)的是,在過去的幾年內(nèi) JVM 有了很大的改進(jìn),大大減少了與正確使用同步相關(guān)的性能損失。通過仔細(xì)分析在線程間如何共享數(shù)據(jù),適當(dāng)?shù)赝綄?duì)共享數(shù)據(jù)的操作,可以使得您的程序既是線程安全的,又不會(huì)承受過多的性能負(fù)擔(dān)。

    參考資料

    關(guān)于作者
    Brian Goetz 是一名軟件顧問,并且過去 15 年來(lái)一直是專業(yè)的軟件開發(fā)人員。他是 Quiotix,一家坐落在 Los Altos,California 的軟件開發(fā)和咨詢公司的首席顧問。請(qǐng)通過 brian@quiotix.com 與 Brian 聯(lián)系。
    posted on 2005-06-24 08:57 蘋果 閱讀(444) 評(píng)論(0)  編輯  收藏 所屬分類: J2EE/JAVA學(xué)習(xí)
    主站蜘蛛池模板: 亚洲aⅴ天堂av天堂无码麻豆| 久久精品国产亚洲AV麻豆~| 亚洲一线产区二线产区区| 1000部啪啪毛片免费看| 亚洲电影一区二区| 免费福利电影在线观看| 久久久久久a亚洲欧洲AV| 日韩精品在线免费观看| 久久精品国产亚洲av成人| 免费在线观看一级片| 久久亚洲AV成人无码软件 | 精品97国产免费人成视频 | 91av免费观看| 亚洲国产视频网站| 免费a级毛片高清视频不卡 | 四虎永久精品免费观看| 免费国产高清毛不卡片基地| 亚洲一区二区三区免费| 你懂的网址免费国产| 亚洲色图国产精品| 免费观看成人毛片a片2008| 国产在亚洲线视频观看| 亚洲欧洲∨国产一区二区三区| 嫩草在线视频www免费观看| 亚洲国产中文在线视频| 国产免费av一区二区三区| 国产精品福利片免费看| 亚洲最新永久在线观看| 成年女人男人免费视频播放| 免费很黄无遮挡的视频毛片| 国产AV无码专区亚洲AVJULIA| 免费A级毛片无码A∨免费| 亚洲AV色无码乱码在线观看| 亚洲性猛交XXXX| 青青草免费在线视频| 一区二区三区AV高清免费波多| 亚洲网址在线观看你懂的| 成人激情免费视频| 久久99久久成人免费播放| 亚洲视频一区二区三区四区| 亚洲五月午夜免费在线视频|