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

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

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

    Vincent

    Vicent's blog
    隨筆 - 74, 文章 - 0, 評(píng)論 - 5, 引用 - 0
    數(shù)據(jù)加載中……

    輕松使用線程: 同步不是敵人

    與許多其它的編程語(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)橐恢币詠恚@些問題都是留給平臺(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 程序中,用來管理線程間協(xié)調(diào)工作的主要工具是 synchronized 關(guān)鍵字。在缺少同步的情況下,JVM 可以很自由地對(duì)不同線程內(nèi)執(zhí)行的操作進(jìn)行計(jì)時(shí)和排序。在大部分情況下,這正是我們想要的,因?yàn)檫@樣可以提高性能,但它也給程序員帶來了額外的負(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è)首


    使用一條好的運(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)境有很大的不同,無論是配置的不同,還是負(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)。





    回頁(yè)首


    同步的代價(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à),并且將來還可能會(huì)有進(jìn)一步的改進(jìn)。此外,同步的性能代價(jià)經(jīng)常是被夸大的。一個(gè)著名的資料來源就曾經(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è)無爭(zhēng)用的同步給一個(gè)塊或方法帶來的是固定的性能損失。而這一固定的延遲帶來的性能損失百分比取決于在該同步塊內(nèi)做了多少工作。對(duì)一個(gè) 方法的同步調(diào)用可能要比對(duì)一個(gè)空方法的非同步調(diào)用慢 20 倍,但我們多長(zhǎng)時(shí)間才調(diào)用一次空方法呢?當(dāng)我們用更有代表性的小方法來衡量同步損失時(shí),百分?jǐn)?shù)很快就下降到可以容忍的范圍之內(nèi)。

    表 1 把一些這種數(shù)據(jù)放在一起來看。它列舉了一些不同的實(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)出來。在大多數(shù)測(cè)試中,每個(gè) JVM 的更高版本都會(huì)使 JVM 的總體性能得到很大提高,很有可能 1.4 版的 Java 虛擬機(jī)發(fā)行的時(shí)候,它的性能還會(huì)有進(jìn)一步的提高。

    表 1. 無爭(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 能否這樣做就要取決于很多因素了,所以,無 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è)無爭(zhēng)用同步的代價(jià)要比創(chuàng)建一個(gè)對(duì)象或查找一個(gè) HashMap 的代價(jià)小。

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





    回頁(yè)首


    不要爭(zhēng)用

    假設(shè)同步使用正確,若線程真正參與爭(zhēng)用加鎖,您也能感受到同步對(duì)實(shí)際性能的影響。并且無爭(zhēng)用同步和爭(zhēng)用同步間的性能損失差別很大;一個(gè)簡(jiǎn)單的測(cè)試程序指出爭(zhēng)用同步比無爭(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ù)量。





    回頁(yè)首


    什么時(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)是明確指出使用什么鎖來保護(hù)給定的字段或?qū)ο螅⒃谀愕拇a里把它記錄下來。

    還有一點(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)用者能夠存取到一致的和最近版本的屬性值,但如果希望屬性的將來值與當(dāng)前值一致,或多個(gè)屬性間相互一致,我們就必須同步復(fù)合操作 ― 可能是在一個(gè)粗粒度的鎖上。





    回頁(yè)首


    如果情況不確定,考慮使用同步包裝

    有時(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è)同步的版本來包裝每個(gè)方法。





    回頁(yè)首


    結(jié)論

    雖然 JLS 給了我們可以使我們的程序線程安全的工具,但線程安全也不是天上掉下來的餡餅。使用同步會(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)。

    posted @ 2006-08-24 17:44 Binary 閱讀(282) | 評(píng)論 (0)編輯 收藏

    Java 理論與實(shí)踐: 做個(gè)好的(事件)偵聽器

    觀察者模式在 Swing 開發(fā)中很常見,在 GUI 應(yīng)用程序以外的場(chǎng)景中,它對(duì)于消除組件的耦合性也非常有用。但是,仍然存在一些偵聽器登記和調(diào)用方面的常見缺陷。在 Java 理論與實(shí)踐 的這一期中,Java 專家 Brian Goetz 就如何做一個(gè)好的偵聽器,以及如何對(duì)您的偵聽器也友好,提供了一些感覺很好的建議。請(qǐng)?jiān)谙鄳?yīng)的 討論論壇 上與作者和其他讀者分享您對(duì)這篇文章的想法。(您也可以單擊本文頂部或底部的 討論 訪問論壇。)

    Swing 框架以事件偵聽器的形式廣泛利用了觀察者模式(也稱為發(fā)布-訂閱模式)。Swing 組件作為用戶交互的目標(biāo),在用戶與它們交互的時(shí)候觸發(fā)事件;數(shù)據(jù)模型類在數(shù)據(jù)發(fā)生變化時(shí)觸發(fā)事件。用這種方式使用觀察者,可以讓控制器與模型分離,讓模型與視圖分離,從而簡(jiǎn)化 GUI 應(yīng)用程序的開發(fā)。

    “四人幫”的 設(shè)計(jì)模式 一書(參閱 參考資料)把觀察者模式描述為:定義對(duì)象之間的“一對(duì)多”關(guān)系,這樣一個(gè)對(duì)象改變狀態(tài)時(shí),所有它的依賴項(xiàng)都會(huì)被通知,并自動(dòng)更新。觀察者模式支持組件之間的松散耦合;組件可以保持它們的狀態(tài)同步,卻不需要直接知道彼此的標(biāo)識(shí)或內(nèi)部情況,從而促進(jìn)了組件的重用。

    AWT 和 Swing 組件(例如 JButtonJTable)使用觀察者模式消除了 GUI 事件生成與它們?cè)谥付☉?yīng)用程序中的語(yǔ)義之間的耦合。類似地,Swing 的模型類,例如 TableModelTreeModel,也使用觀察者消除數(shù)據(jù)模型表示 與視圖生成之間的耦合,從而支持相同數(shù)據(jù)的多個(gè)獨(dú)立的視圖。Swing 定義了 EventEventListener 對(duì)象層次結(jié)構(gòu);可以生成事件的組件,例如 JButton(可視組件) 或 TableModel(數(shù)據(jù)模型),提供了 addXxxListener()removeXxxListener() 方法,用于偵聽器的登記和取消登記。這些類負(fù)責(zé)決定什么時(shí)候它們需要觸發(fā)事件,什么時(shí)候確實(shí)觸發(fā)事件,以及什么時(shí)候調(diào)用所有登記的偵聽器。

    為了支持偵聽器,對(duì)象需要維護(hù)一個(gè)已登記的偵聽器列表,提供偵聽器登記和取消登記的手段,并在適當(dāng)?shù)氖录l(fā)生時(shí)調(diào)用每個(gè)偵聽器。使用和支持偵聽器很容易(不僅僅在 GUI 應(yīng)用程序中),但是在登記接口的兩邊(它們是支持偵聽器的組件和登記偵聽器的組件)都應(yīng)當(dāng)避免一些缺陷。

    線程安全問題

    通常,調(diào)用偵聽器的線程與登記偵聽器的線程不同。要支持從不同線程登記偵聽器,那么不管用什么機(jī)制存儲(chǔ)和管理活動(dòng)偵聽器列表,這個(gè)機(jī)制都必須是線程安全的。Sun 的文檔中的許多示例使用 Vector 保存?zhèn)陕犉髁斜恚鉀Q了部分問題,但是沒有解決全部問題。在事件觸發(fā)時(shí),觸發(fā)它的組件會(huì)考慮迭代偵聽器列表,并調(diào)用每個(gè)偵聽器,這就帶來了并發(fā)修改的風(fēng)險(xiǎn),比如在偵聽器列表迭代期間,某個(gè)線程偶然想添加或刪除一個(gè)偵聽器。

    管理偵聽器列表

    假設(shè)您使用 Vector<Listener> 保存?zhèn)陕犉髁斜怼km然 Vector 類是線程安全的(意味著不需要進(jìn)行額外的同步就可調(diào)用它的方法,沒有破壞 Vector 數(shù)據(jù)結(jié)構(gòu)的風(fēng)險(xiǎn)),但是集合的迭代中包含“檢測(cè)然后執(zhí)行”序列,如果在迭代期間集合被修改,就有了失敗的風(fēng)險(xiǎn)。假設(shè)迭代開始時(shí)列表中有三個(gè)偵聽器。在迭代 Vector 時(shí),重復(fù)調(diào)用 size()get() 方法,直到所有元素都檢索完,如清單 1 所示:


    清單 1. Vector 的不安全迭代
    												
    														Vector<Listener> v;
    for (int i=0; i<v.size(); i++)
      v.get(i).eventHappened(event);
    
    												
    										

    但是,如果恰好就在最后一次調(diào)用 Vector.size() 之后,有人從列表中刪除了一個(gè)偵聽器,會(huì)發(fā)生什么呢?現(xiàn)在,Vector.get() 將返回 null (這是對(duì)的,因?yàn)閺纳洗螜z測(cè) vector 的狀態(tài)以來,它的狀態(tài)已經(jīng)變了),而在試圖調(diào)用 eventHappened() 時(shí),會(huì)拋出 NullPointerException。這是“檢測(cè)然后執(zhí)行”序列的一個(gè)示例 —— 檢測(cè)是否存在更多元素,如果存在,就取得下一元素 —— 但是在存在并發(fā)修改的情況下,檢測(cè)之后狀態(tài)可能已經(jīng)變化。圖 1 演示了這個(gè)問題:

    圖 1. 并發(fā)迭代和修改,造成意料之外的失敗

    并發(fā)迭代和修改,造成意料之外的失敗

    這個(gè)問題的一個(gè)解決方案是在迭代期間持有對(duì) Vector 的鎖;另一個(gè)方案是克隆 Vector 或調(diào)用它的 toArray() 方法,在每次發(fā)生事件時(shí)檢索它的內(nèi)容。所有這兩個(gè)方法都有性能上的問題:第一個(gè)的風(fēng)險(xiǎn)是在迭代期間,會(huì)把其他想訪問偵聽器列表的線程鎖在外面;第二個(gè)則要?jiǎng)?chuàng)建臨時(shí)對(duì)象,而且每次事件發(fā)生時(shí)都要拷貝列表。

    如果用迭代器(Iterator)去遍歷偵聽器列表,也會(huì)有同樣的問題,只是表現(xiàn)略有不同; iterator() 實(shí)現(xiàn)不拋出 NullPointerException,它在探測(cè)到迭代開始之后集合發(fā)生修改時(shí),會(huì)拋出 ConcurrentModificationException。同樣,也可以通過在迭代期間鎖定集合防止這個(gè)問題。

    java.util.concurrent 中的 CopyOnWriteArrayList 類,能夠幫助防止這個(gè)問題。它實(shí)現(xiàn)了 List,而且是線程安全的,但是它的迭代器不會(huì)拋出 ConcurrentModificationException,遍歷期間也不要求額外的鎖定。這種特性組合是通過在每次列表修改時(shí),在內(nèi)部重新分配并拷貝列表內(nèi)容而實(shí)現(xiàn)的,這樣,遍歷內(nèi)容的線程不需要處理變化 —— 從它們的角度來說,列表的內(nèi)容在遍歷期間保持不變。雖然這聽起來可能沒效率,但是請(qǐng)記住,在多數(shù)觀察者情況下,每個(gè)組件只有少量偵聽器,遍歷的數(shù)量遠(yuǎn)遠(yuǎn)超過插入和刪除的數(shù)量。所以更快的迭代可以補(bǔ)償較慢的變化過程,并提供更好的并發(fā)性,因?yàn)槎鄠€(gè)線程可以同時(shí)迭代列表。

    初始化的安全風(fēng)險(xiǎn)

    從偵聽器的構(gòu)造函數(shù)中登記它很誘惑人,但是這是一個(gè)應(yīng)當(dāng)避免的誘惑。它僅會(huì)造成“失效偵聽器(lapsed listener)的問題(我稍后討論它),而且還會(huì)造成多個(gè)線程安全問題。清單 2 顯示了一個(gè)看起來沒什么害處的同時(shí)構(gòu)造和登記偵聽器的企圖。問題是:它造成到對(duì)象的“this”引用在對(duì)象完全構(gòu)造完成之前轉(zhuǎn)義。雖然看起來沒什么害處,因?yàn)榈怯浭菢?gòu)造函數(shù)做的最后一件事,但是看到的東西是有欺騙性的:


    清單 2. 事件偵聽器允許“this”引用轉(zhuǎn)義,造成問題
    												
    														public class EventListener { 
    
      public EventListener(EventSource eventSource) {
        // do our initialization
        ...
    
        // register ourselves with the event source
        eventSource.registerListener(this);
      }
    
      public onEvent(Event e) { 
        // handle the event
      }
    }
    
    												
    										

    在繼承事件偵聽器的時(shí)候,會(huì)出現(xiàn)這種方法的一個(gè)風(fēng)險(xiǎn):這時(shí),子類構(gòu)造函數(shù)做的任何工作都是在 EventListener 構(gòu)造函數(shù)運(yùn)行之后進(jìn)行的,也就是在 EventListener 發(fā)布之后,所以會(huì)造成爭(zhēng)用情況。在某些不幸的時(shí)候,清單 3 中的 onEvent 方法會(huì)在列表字段還沒初始化之前就被調(diào)用,從而在取消 final 字段的引用時(shí),會(huì)生成非常讓人困惑的 NullPointerException 異常:


    清單 3. 繼承清單 2 的 EventListener 類造成的問題
    												
    														public class RecordingEventListener extends EventListener {
      private final ArrayList<Event> list;
    
      public RecordingEventListener(EventSource eventSource) {
        super(eventSource);
        list = Collections.synchronizedList(new ArrayList<Event>());
      }
    
      public onEvent(Event e) { 
        list.add(e);
        super.onEvent(e);
      }
    }
    
    												
    										

    即使偵聽器類是 final 的,不能派生子類,也不應(yīng)當(dāng)允許“this”引用在構(gòu)造函數(shù)中轉(zhuǎn)義 —— 這樣做會(huì)危害 Java 內(nèi)存模型的某些安全保證。如果“this”這個(gè)詞不會(huì)出現(xiàn)在程序中,就可讓“this”引用轉(zhuǎn)義;發(fā)布一個(gè)非靜態(tài)內(nèi)部類實(shí)例可以達(dá)到相同的效果,因?yàn)閮?nèi)部類持有對(duì)它包圍的對(duì)象的“this”引用的引用。偶然地允許“this”引用轉(zhuǎn)義的最常見原因,就是登記偵聽器,如清單 4 所示。事件偵聽器不應(yīng)當(dāng)在構(gòu)造函數(shù)中登記!


    清單 4. 通過發(fā)布內(nèi)部類實(shí)例,顯式地允許“this”引用轉(zhuǎn)義
    												
    														public class EventListener2 {
      public EventListener2(EventSource eventSource) {
    
        eventSource.registerListener(
          new EventListener() {
            public void onEvent(Event e) { 
              eventReceived(e);
            }
          });
      }
    
      public void eventReceived(Event e) {
      }
    }
    
    												
    										

    偵聽器線程安全

    使用偵聽器造成的第三個(gè)線程安全問題來自這個(gè)事實(shí):偵聽器可能想訪問應(yīng)用程序數(shù)據(jù),而調(diào)用偵聽器的線程通常不直接在應(yīng)用程序的控制之下。如果在 JButton 或其他 Swing 組件上登記偵聽器,那么會(huì)從 EDT 調(diào)用該偵聽器。偵聽器的代碼可以從 EDT 安全地調(diào)用 Swing 組件上的方法,但是如果對(duì)象本身不是線程安全的,那么從偵聽器訪問應(yīng)用程序?qū)ο髸?huì)給應(yīng)用程序增加新的線程安全需求。

    Swing 組件生成的事件是用戶交互的結(jié)果,但是 Swing 模型類是在 fireXxxEvent() 方法被調(diào)用的時(shí)候生成事件。這些方法又會(huì)在調(diào)用它們的線程中調(diào)用偵聽器。因?yàn)?Swing 模型類不是線程安全的,而且假設(shè)被限制在 EDT 內(nèi),所以對(duì) fireXxxEvent() 的任何調(diào)用也都應(yīng)當(dāng)從 EDT 執(zhí)行。如果想從另外的線程觸發(fā)事件,那么應(yīng)當(dāng)用 Swing 的 invokeLater() 功能讓方法轉(zhuǎn)而在 EDT 內(nèi)調(diào)用。一般來說,要注意調(diào)用事件偵聽器的線程,還要保證它們涉及的任何對(duì)象或者是線程安全的,或者在訪問它們的地方,受到適當(dāng)?shù)耐剑ɑ蛘呤?Swing 模型類的線程約束)的保護(hù)。





    回頁(yè)首


    失效偵聽器

    不管什么時(shí)候使用觀察者模式,都耦合著兩個(gè)獨(dú)立組件 —— 觀察者和被觀察者,它們通常有不同的生命周期。登記偵聽器的后果之一就是:它在被觀察對(duì)象和偵聽器之間建立起很強(qiáng)的引用關(guān)系,這種關(guān)系防止偵聽器(以及它引用的對(duì)象)被垃圾收集,直到偵聽器取消登記為止。在許多情況下,偵聽器的生命周期至少要和被觀察的組件一樣長(zhǎng) —— 許多偵聽器會(huì)在整個(gè)應(yīng)用程序期間都存在。但是在某些情況下,應(yīng)當(dāng)短期存在的偵聽器最后變成了永久的,它們這種無意識(shí)的拖延的證據(jù)就是應(yīng)用程序性能變慢、高于必需的內(nèi)存使用。

    “失效偵聽器”的問題可以由設(shè)計(jì)級(jí)別上的不小心造成:沒有恰當(dāng)?shù)乜紤]包含的對(duì)象的壽命,或者由于松懈的編碼。偵聽器登記和取消登記應(yīng)當(dāng)結(jié)對(duì)進(jìn)行。但是即使這么做,也必須保證是在正確的時(shí)間執(zhí)行取消登記。清單 5 顯示了會(huì)造成失效偵聽器的編碼習(xí)慣的示例。它在組件上登記偵聽器,執(zhí)行某些動(dòng)作,然后取消登記偵聽器:


    清單 5. 有造成失效偵聽器風(fēng)險(xiǎn)的代碼
    												
    														  public void processFile(String filename) throws IOException {
        cancelButton.registerListener(this);
        // open file, read it, process it
        // might throw IOException
        cancelButton.unregisterListener(this);
      }
    
    												
    										

    清單 5 的問題是:如果文件處理代碼拋出了 IOException —— 這是很有可能的 —— 那么偵聽器就永遠(yuǎn)不會(huì)取消登記,這就意味著它永遠(yuǎn)不會(huì)被垃圾收集。取消登記的操作應(yīng)當(dāng)在 finally 塊中進(jìn)行,這樣,processFile() 方法的所有出口都會(huì)執(zhí)行它。

    有時(shí)推薦的一個(gè)處理失效偵聽器的方法是使用弱引用。雖然這種方法可行,但是實(shí)現(xiàn)起來很麻煩。要讓它工作,需要找到另外一個(gè)對(duì)象,它的生命周期恰好是偵聽器的生命周期,并安排它持有對(duì)偵聽器的強(qiáng)引用,這可不是件容易的事。

    另外一項(xiàng)可以用來找到隱藏失效偵聽器的技術(shù)是:防止指定偵聽器對(duì)象在指定事件源上登記兩次。這種情況通常是 bug 的跡象 —— 偵聽器登記了,但是沒有取消登記,然后再次登記。不用檢測(cè)問題,就能緩解這個(gè)問題的影響的一種方式是:使用 Set 代替 List 來存儲(chǔ)偵聽器;或者也可以檢測(cè) List,在登記偵聽器之前檢查是否已經(jīng)登記了,如果已經(jīng)登記,就拋出異常(或記錄錯(cuò)誤),這樣就可以搜集編碼錯(cuò)誤的證據(jù),并采取行動(dòng)。





    回頁(yè)首


    其他偵聽器問題

    在編寫偵聽器時(shí),應(yīng)當(dāng)一直注意它們將要執(zhí)行的環(huán)境。不僅要注意線程安全問題,還需要記住:偵聽器也可以用其他方式為它的調(diào)用者把事情搞糟。偵聽器 不該 做的一件事是:阻塞相當(dāng)長(zhǎng)一段時(shí)間(長(zhǎng)得可以感覺得到);調(diào)用它的執(zhí)行上下文很可能希望迅速返回控制。如果偵聽器要執(zhí)行一個(gè)可能比較費(fèi)時(shí)的操作,例如處理大型文本,或者要做的工作可能阻塞,例如執(zhí)行 socket IO,那么偵聽器應(yīng)當(dāng)把這些操作安排在另一個(gè)線程中進(jìn)行,這樣它就可以迅速返回它的調(diào)用者。

    對(duì)于不小心的事件源,偵聽器會(huì)造成麻煩的另一個(gè)方式是:拋出未檢測(cè)的異常。雖然大多數(shù)時(shí)候,我們不會(huì)故意拋出未檢測(cè)異常,但是確實(shí)有些時(shí)候會(huì)發(fā)生這種情況。如果使用清單 1 的方式調(diào)用偵聽器,列表中的第二個(gè)偵聽器就會(huì)拋出未檢測(cè)異常,那么不僅后續(xù)的偵聽器得不到調(diào)用(可能造成應(yīng)用程序處在不一致的狀態(tài)),而且有可能把執(zhí)行它的線程破壞掉,從而造成局部應(yīng)用程序失敗。

    在調(diào)用未知代碼(偵聽器就是這樣的代碼)時(shí),謹(jǐn)慎的方式是在 try-catch 塊中執(zhí)行它,這樣,行為有誤的偵聽器不會(huì)造成更多不必要的破壞。對(duì)于拋出未檢測(cè)異常的偵聽器,您可能想自動(dòng)對(duì)它取消登記,畢竟,拋出未檢測(cè)異常就證明偵聽器壞掉了。(您可能還想記錄這個(gè)錯(cuò)誤或者提醒用戶注意,好讓用戶能夠知道為什么程序停止像期望的那樣繼續(xù)工作。)清單 6 顯示了這種方式的一個(gè)示例,它在迭代循環(huán)內(nèi)部嵌套了 try-catch 塊:


    清單 6. 健壯的偵聽器調(diào)用
    												
    														List<Listener> list;
    for (Iterator<Listener> i=list.iterator; i.hasNext(); ) {
        Listener l = i.next();
        try {
            l.eventHappened(event);
        }
        catch (RuntimeException e) {
            log("Unexpected exception in listener", e);
            i.remove();
        }
    }
    
    												
    										





    回頁(yè)首


    結(jié)束語(yǔ)

    觀察者模式對(duì)于創(chuàng)建松散耦合的組件、鼓勵(lì)組件重用非常有用,但是它有一些風(fēng)險(xiǎn),偵聽器的編寫者和組件的編寫者都應(yīng)當(dāng)注意。在登記偵聽器時(shí),應(yīng)當(dāng)一直注意偵聽器的生命周期。如果偵聽器的壽命應(yīng)當(dāng)比應(yīng)用程序的短,那么請(qǐng)確保取消它的登記,這樣它就可以被垃圾收集。在編寫偵聽器和組件時(shí),請(qǐng)注意它包含的線程安全性問題。偵聽器涉及的任何對(duì)象,都應(yīng)當(dāng)是線程安全的,或者是受線程約束的對(duì)象(例如 Swing 模型),偵聽器應(yīng)當(dāng)確定自己正在正確的線程中執(zhí)行。

    posted @ 2006-08-24 17:43 Binary 閱讀(231) | 評(píng)論 (0)編輯 收藏

    Java 理論與實(shí)踐: 用弱引用堵住內(nèi)存泄漏

    雖然用 Java? 語(yǔ)言編寫的程序在理論上是不會(huì)出現(xiàn)“內(nèi)存泄漏”的,但是有時(shí)對(duì)象在不再作為程序的邏輯狀態(tài)的一部分之后仍然不被垃圾收集。本月,負(fù)責(zé)保障應(yīng)用程序健康的工程師 Brian Goetz 探討了無意識(shí)的對(duì)象保留的常見原因,并展示了如何用弱引用堵住泄漏。

    要讓垃圾收集(GC)回收程序不再使用的對(duì)象,對(duì)象的邏輯 生命周期(應(yīng)用程序使用它的時(shí)間)和對(duì)該對(duì)象擁有的引用的實(shí)際 生命周期必須是相同的。在大多數(shù)時(shí)候,好的軟件工程技術(shù)保證這是自動(dòng)實(shí)現(xiàn)的,不用我們對(duì)對(duì)象生命周期問題花費(fèi)過多心思。但是偶爾我們會(huì)創(chuàng)建一個(gè)引用,它在內(nèi)存中包含對(duì)象的時(shí)間比我們預(yù)期的要長(zhǎng)得多,這種情況稱為無意識(shí)的對(duì)象保留(unintentional object retention)

    全局 Map 造成的內(nèi)存泄漏

    無意識(shí)對(duì)象保留最常見的原因是使用 Map 將元數(shù)據(jù)與臨時(shí)對(duì)象(transient object)相關(guān)聯(lián)。假定一個(gè)對(duì)象具有中等生命周期,比分配它的那個(gè)方法調(diào)用的生命周期長(zhǎng),但是比應(yīng)用程序的生命周期短,如客戶機(jī)的套接字連接。需要將一些元數(shù)據(jù)與這個(gè)套接字關(guān)聯(lián),如生成連接的用戶的標(biāo)識(shí)。在創(chuàng)建 Socket 時(shí)是不知道這些信息的,并且不能將數(shù)據(jù)添加到 Socket 對(duì)象上,因?yàn)椴荒芸刂?Socket 類或者它的子類。這時(shí),典型的方法就是在一個(gè)全局 Map 中存儲(chǔ)這些信息,如清單 1 中的 SocketManager 類所示:


    清單 1. 使用一個(gè)全局 Map 將元數(shù)據(jù)關(guān)聯(lián)到一個(gè)對(duì)象
    												
    														public class SocketManager {
        private Map<Socket,User> m = new HashMap<Socket,User>();
        
        public void setUser(Socket s, User u) {
            m.put(s, u);
        }
        public User getUser(Socket s) {
            return m.get(s);
        }
        public void removeUser(Socket s) {
            m.remove(s);
        }
    }
    
    SocketManager socketManager;
    ...
    socketManager.setUser(socket, user);
    
    												
    										

    這種方法的問題是元數(shù)據(jù)的生命周期需要與套接字的生命周期掛鉤,但是除非準(zhǔn)確地知道什么時(shí)候程序不再需要這個(gè)套接字,并記住從 Map 中刪除相應(yīng)的映射,否則,SocketUser 對(duì)象將會(huì)永遠(yuǎn)留在 Map 中,遠(yuǎn)遠(yuǎn)超過響應(yīng)了請(qǐng)求和關(guān)閉套接字的時(shí)間。這會(huì)阻止 SocketUser 對(duì)象被垃圾收集,即使應(yīng)用程序不會(huì)再使用它們。這些對(duì)象留下來不受控制,很容易造成程序在長(zhǎng)時(shí)間運(yùn)行后內(nèi)存爆滿。除了最簡(jiǎn)單的情況,在幾乎所有情況下找出什么時(shí)候 Socket 不再被程序使用是一件很煩人和容易出錯(cuò)的任務(wù),需要人工對(duì)內(nèi)存進(jìn)行管理。





    回頁(yè)首


    找出內(nèi)存泄漏

    程序有內(nèi)存泄漏的第一個(gè)跡象通常是它拋出一個(gè) OutOfMemoryError,或者因?yàn)轭l繁的垃圾收集而表現(xiàn)出糟糕的性能。幸運(yùn)的是,垃圾收集可以提供能夠用來診斷內(nèi)存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 選項(xiàng)調(diào)用 JVM,那么每次 GC 運(yùn)行時(shí)在控制臺(tái)上或者日志文件中會(huì)打印出一個(gè)診斷信息,包括它所花費(fèi)的時(shí)間、當(dāng)前堆使用情況以及恢復(fù)了多少內(nèi)存。記錄 GC 使用情況并不具有干擾性,因此如果需要分析內(nèi)存問題或者調(diào)優(yōu)垃圾收集器,在生產(chǎn)環(huán)境中默認(rèn)啟用 GC 日志是值得的。

    有工具可以利用 GC 日志輸出并以圖形方式將它顯示出來,JTune 就是這樣的一種工具(請(qǐng)參閱 參考資料)。觀察 GC 之后堆大小的圖,可以看到程序內(nèi)存使用的趨勢(shì)。對(duì)于大多數(shù)程序來說,可以將內(nèi)存使用分為兩部分:baseline 使用和 current load 使用。對(duì)于服務(wù)器應(yīng)用程序,baseline 使用就是應(yīng)用程序在沒有任何負(fù)荷、但是已經(jīng)準(zhǔn)備好接受請(qǐng)求時(shí)的內(nèi)存使用,current load 使用是在處理請(qǐng)求過程中使用的、但是在請(qǐng)求處理完成后會(huì)釋放的內(nèi)存。只要負(fù)荷大體上是恒定的,應(yīng)用程序通常會(huì)很快達(dá)到一個(gè)穩(wěn)定的內(nèi)存使用水平。如果在應(yīng)用程序已經(jīng)完成了其初始化并且負(fù)荷沒有增加的情況下,內(nèi)存使用持續(xù)增加,那么程序就可能在處理前面的請(qǐng)求時(shí)保留了生成的對(duì)象。

    清單 2 展示了一個(gè)有內(nèi)存泄漏的程序。MapLeaker 在線程池中處理任務(wù),并在一個(gè) Map 中記錄每一項(xiàng)任務(wù)的狀態(tài)。不幸的是,在任務(wù)完成后它不會(huì)刪除那一項(xiàng),因此狀態(tài)項(xiàng)和任務(wù)對(duì)象(以及它們的內(nèi)部狀態(tài))會(huì)不斷地積累。


    清單 2. 具有基于 Map 的內(nèi)存泄漏的程序
    												
    														public class MapLeaker {
        public ExecutorService exec = Executors.newFixedThreadPool(5);
        public Map<Task, TaskStatus> taskStatus 
            = Collections.synchronizedMap(new HashMap<Task, TaskStatus>());
        private Random random = new Random();
    
        private enum TaskStatus { NOT_STARTED, STARTED, FINISHED };
    
        private class Task implements Runnable {
            private int[] numbers = new int[random.nextInt(200)];
    
            public void run() {
                int[] temp = new int[random.nextInt(10000)];
                taskStatus.put(this, TaskStatus.STARTED);
                doSomeWork();
                taskStatus.put(this, TaskStatus.FINISHED);
            }
        }
    
        public Task newTask() {
            Task t = new Task();
            taskStatus.put(t, TaskStatus.NOT_STARTED);
            exec.execute(t);
            return t;
        }
    }
    
    												
    										

    圖 1 顯示 MapLeaker GC 之后應(yīng)用程序堆大小隨著時(shí)間的變化圖。上升趨勢(shì)是存在內(nèi)存泄漏的警示信號(hào)。(在真實(shí)的應(yīng)用程序中,坡度不會(huì)這么大,但是在收集了足夠長(zhǎng)時(shí)間的 GC 數(shù)據(jù)后,上升趨勢(shì)通常會(huì)表現(xiàn)得很明顯。)


    圖 1. 持續(xù)上升的內(nèi)存使用趨勢(shì)

    確信有了內(nèi)存泄漏后,下一步就是找出哪種對(duì)象造成了這個(gè)問題。所有內(nèi)存分析器都可以生成按照對(duì)象類進(jìn)行分解的堆快照。有一些很好的商業(yè)堆分析工具,但是找出內(nèi)存泄漏不一定要花錢買這些工具 —— 內(nèi)置的 hprof 工具也可完成這項(xiàng)工作。要使用 hprof 并讓它跟蹤內(nèi)存使用,需要以 -Xrunhprof:heap=sites 選項(xiàng)調(diào)用 JVM。

    清單 3 顯示分解了應(yīng)用程序內(nèi)存使用的 hprof 輸出的相關(guān)部分。(hprof 工具在應(yīng)用程序退出時(shí),或者用 kill -3 或在 Windows 中按 Ctrl+Break 時(shí)生成使用分解。)注意兩次快照相比,Map.EntryTaskint[] 對(duì)象有了顯著增加。

    請(qǐng)參閱 清單 3

    清單 4 展示了 hprof 輸出的另一部分,給出了 Map.Entry 對(duì)象的分配點(diǎn)的調(diào)用堆棧信息。這個(gè)輸出告訴我們哪些調(diào)用鏈生成了 Map.Entry 對(duì)象,并帶有一些程序分析,找出內(nèi)存泄漏來源一般來說是相當(dāng)容易的。


    清單 4. HPROF 輸出,顯示 Map.Entry 對(duì)象的分配點(diǎn)
    												
    														TRACE 300446:
    	java.util.HashMap$Entry.<init>(<Unknown Source>:Unknown line)
    	java.util.HashMap.addEntry(<Unknown Source>:Unknown line)
    	java.util.HashMap.put(<Unknown Source>:Unknown line)
    	java.util.Collections$SynchronizedMap.put(<Unknown Source>:Unknown line)
    	com.quiotix.dummy.MapLeaker.newTask(MapLeaker.java:48)
    	com.quiotix.dummy.MapLeaker.main(MapLeaker.java:64)
    
    												
    										





    回頁(yè)首


    弱引用來救援了

    SocketManager 的問題是 Socket-User 映射的生命周期應(yīng)當(dāng)與 Socket 的生命周期相匹配,但是語(yǔ)言沒有提供任何容易的方法實(shí)施這項(xiàng)規(guī)則。這使得程序不得不使用人工內(nèi)存管理的老技術(shù)。幸運(yùn)的是,從 JDK 1.2 開始,垃圾收集器提供了一種聲明這種對(duì)象生命周期依賴性的方法,這樣垃圾收集器就可以幫助我們防止這種內(nèi)存泄漏 —— 利用弱引用

    弱引用是對(duì)一個(gè)對(duì)象(稱為 referent)的引用的持有者。使用弱引用后,可以維持對(duì) referent 的引用,而不會(huì)阻止它被垃圾收集。當(dāng)垃圾收集器跟蹤堆的時(shí)候,如果對(duì)一個(gè)對(duì)象的引用只有弱引用,那么這個(gè) referent 就會(huì)成為垃圾收集的候選對(duì)象,就像沒有任何剩余的引用一樣,而且所有剩余的弱引用都被清除。(只有弱引用的對(duì)象稱為弱可及(weakly reachable)。)

    WeakReference 的 referent 是在構(gòu)造時(shí)設(shè)置的,在沒有被清除之前,可以用 get() 獲取它的值。如果弱引用被清除了(不管是 referent 已經(jīng)被垃圾收集了,還是有人調(diào)用了 WeakReference.clear()),get() 會(huì)返回 null。相應(yīng)地,在使用其結(jié)果之前,應(yīng)當(dāng)總是檢查 get() 是否返回一個(gè)非 null 值,因?yàn)?referent 最終總是會(huì)被垃圾收集的。

    用一個(gè)普通的(強(qiáng))引用拷貝一個(gè)對(duì)象引用時(shí),限制 referent 的生命周期至少與被拷貝的引用的生命周期一樣長(zhǎng)。如果不小心,那么它可能就與程序的生命周期一樣 —— 如果將一個(gè)對(duì)象放入一個(gè)全局集合中的話。另一方面,在創(chuàng)建對(duì)一個(gè)對(duì)象的弱引用時(shí),完全沒有擴(kuò)展 referent 的生命周期,只是在對(duì)象仍然存活的時(shí)候,保持另一種到達(dá)它的方法。

    弱引用對(duì)于構(gòu)造弱集合最有用,如那些在應(yīng)用程序的其余部分使用對(duì)象期間存儲(chǔ)關(guān)于這些對(duì)象的元數(shù)據(jù)的集合 —— 這就是 SocketManager 類所要做的工作。因?yàn)檫@是弱引用最常見的用法,WeakHashMap 也被添加到 JDK 1.2 的類庫(kù)中,它對(duì)鍵(而不是對(duì)值)使用弱引用。如果在一個(gè)普通 HashMap 中用一個(gè)對(duì)象作為鍵,那么這個(gè)對(duì)象在映射從 Map 中刪除之前不能被回收,WeakHashMap 使您可以用一個(gè)對(duì)象作為 Map 鍵,同時(shí)不會(huì)阻止這個(gè)對(duì)象被垃圾收集。清單 5 給出了 WeakHashMapget() 方法的一種可能實(shí)現(xiàn),它展示了弱引用的使用:


    清單 5. WeakReference.get() 的一種可能實(shí)現(xiàn)
    												
    														public class WeakHashMap<K,V> implements Map<K,V> {
    
        private static class Entry<K,V> extends WeakReference<K> 
          implements Map.Entry<K,V> {
            private V value;
            private final int hash;
            private Entry<K,V> next;
            ...
        }
    
        public V get(Object key) {
            int hash = getHash(key);
            Entry<K,V> e = getChain(hash);
            while (e != null) {
                K eKey= e.get();
                if (e.hash == hash && (key == eKey || key.equals(eKey)))
                    return e.value;
                e = e.next;
            }
            return null;
        }
    
    												
    										

    調(diào)用 WeakReference.get() 時(shí),它返回一個(gè)對(duì) referent 的強(qiáng)引用(如果它仍然存活的話),因此不需要擔(dān)心映射在 while 循環(huán)體中消失,因?yàn)閺?qiáng)引用會(huì)防止它被垃圾收集。WeakHashMap 的實(shí)現(xiàn)展示了弱引用的一種常見用法 —— 一些內(nèi)部對(duì)象擴(kuò)展 WeakReference。其原因在下面一節(jié)討論引用隊(duì)列時(shí)會(huì)得到解釋。

    在向 WeakHashMap 中添加映射時(shí),請(qǐng)記住映射可能會(huì)在以后“脫離”,因?yàn)殒I被垃圾收集了。在這種情況下,get() 返回 null,這使得測(cè)試 get() 的返回值是否為 null 變得比平時(shí)更重要了。

    用 WeakHashMap 堵住泄漏

    SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如清單 6 所示。(如果 SocketManager 需要線程安全,那么可以用 Collections.synchronizedMap() 包裝 WeakHashMap)。當(dāng)映射的生命周期必須與鍵的生命周期聯(lián)系在一起時(shí),可以使用這種方法。不過,應(yīng)當(dāng)小心不濫用這種技術(shù),大多數(shù)時(shí)候還是應(yīng)當(dāng)使用普通的 HashMap 作為 Map 的實(shí)現(xiàn)。


    清單 6. 用 WeakHashMap 修復(fù) SocketManager
    												
    														public class SocketManager {
        private Map<Socket,User> m = new WeakHashMap<Socket,User>();
        
        public void setUser(Socket s, User u) {
            m.put(s, u);
        }
        public User getUser(Socket s) {
            return m.get(s);
        }
    }
    
    												
    										

    引用隊(duì)列

    WeakHashMap 用弱引用承載映射鍵,這使得應(yīng)用程序不再使用鍵對(duì)象時(shí)它們可以被垃圾收集,get() 實(shí)現(xiàn)可以根據(jù) WeakReference.get() 是否返回 null 來區(qū)分死的映射和活的映射。但是這只是防止 Map 的內(nèi)存消耗在應(yīng)用程序的生命周期中不斷增加所需要做的工作的一半,還需要做一些工作以便在鍵對(duì)象被收集后從 Map 中刪除死項(xiàng)。否則,Map 會(huì)充滿對(duì)應(yīng)于死鍵的項(xiàng)。雖然這對(duì)于應(yīng)用程序是不可見的,但是它仍然會(huì)造成應(yīng)用程序耗盡內(nèi)存,因?yàn)榧词规I被收集了,Map.Entry 和值對(duì)象也不會(huì)被收集。

    可以通過周期性地掃描 Map,對(duì)每一個(gè)弱引用調(diào)用 get(),并在 get() 返回 null 時(shí)刪除那個(gè)映射而消除死映射。但是如果 Map 有許多活的項(xiàng),那么這種方法的效率很低。如果有一種方法可以在弱引用的 referent 被垃圾收集時(shí)發(fā)出通知就好了,這就是引用隊(duì)列 的作用。

    引用隊(duì)列是垃圾收集器向應(yīng)用程序返回關(guān)于對(duì)象生命周期的信息的主要方法。弱引用有兩個(gè)構(gòu)造函數(shù):一個(gè)只取 referent 作為參數(shù),另一個(gè)還取引用隊(duì)列作為參數(shù)。如果用關(guān)聯(lián)的引用隊(duì)列創(chuàng)建弱引用,在 referent 成為 GC 候選對(duì)象時(shí),這個(gè)引用對(duì)象(不是 referent)就在引用清除后加入 到引用隊(duì)列中。之后,應(yīng)用程序從引用隊(duì)列提取引用并了解到它的 referent 已被收集,因此可以進(jìn)行相應(yīng)的清理活動(dòng),如去掉已不在弱集合中的對(duì)象的項(xiàng)。(引用隊(duì)列提供了與 BlockingQueue 同樣的出列模式 —— polled、timed blocking 和 untimed blocking。)

    WeakHashMap 有一個(gè)名為 expungeStaleEntries() 的私有方法,大多數(shù) Map 操作中會(huì)調(diào)用它,它去掉引用隊(duì)列中所有失效的引用,并刪除關(guān)聯(lián)的映射。清單 7 展示了 expungeStaleEntries() 的一種可能實(shí)現(xiàn)。用于存儲(chǔ)鍵-值映射的 Entry 類型擴(kuò)展了 WeakReference,因此當(dāng) expungeStaleEntries() 要求下一個(gè)失效的弱引用時(shí),它得到一個(gè) Entry。用引用隊(duì)列代替定期掃描內(nèi)容的方法來清理 Map 更有效,因?yàn)榍謇磉^程不會(huì)觸及活的項(xiàng),只有在有實(shí)際加入隊(duì)列的引用時(shí)它才工作。


    清單 7. WeakHashMap.expungeStaleEntries() 的可能實(shí)現(xiàn)
    												
    														    private void expungeStaleEntries() {
    	Entry<K,V> e;
            while ( (e = (Entry<K,V>) queue.poll()) != null) {
                int hash = e.hash;
    
                Entry<K,V> prev = getChain(hash);
                Entry<K,V> cur = prev;
                while (cur != null) {
                    Entry<K,V> next = cur.next;
                    if (cur == e) {
                        if (prev == e)
                            setChain(hash, next);
                        else
                            prev.next = next;
                        break;
                    }
                    prev = cur;
                    cur = next;
                }
            }
        }
    
    												
    										





    回頁(yè)首


    結(jié)束語(yǔ)

    弱引用和弱集合是對(duì)堆進(jìn)行管理的強(qiáng)大工具,使得應(yīng)用程序可以使用更復(fù)雜的可及性方案,而不只是由普通(強(qiáng))引用所提供的“要么全部要么沒有”可及性。下個(gè)月,我們將分析與弱引用有關(guān)的軟引用,將分析在使用弱引用和軟引用時(shí),垃圾收集器的行為。

    posted @ 2006-08-24 17:42 Binary 閱讀(243) | 評(píng)論 (0)編輯 收藏

    Java 理論與實(shí)踐: Web 層的狀態(tài)復(fù)制

    大多數(shù)具有一定重要性的 Web 應(yīng)用程序都要求維護(hù)某種會(huì)話狀態(tài),如用戶購(gòu)物車的內(nèi)容。如何在群集服務(wù)器應(yīng)用程序中管理和復(fù)制狀態(tài)對(duì)應(yīng)用程序的可伸縮性有顯著影響。許多 J2SE 和 J2EE 應(yīng)用程序?qū)顟B(tài)存儲(chǔ)在由 Servlet API 提供的 HttpSession 中。本月,專欄作家 Brian Goetz 分析了狀態(tài)復(fù)制的一些選項(xiàng)以及如何最有效地使用 HttpSession 以提供好的伸縮性和性能。在本文論壇中與本文作者和其他讀者分享您的觀點(diǎn)。(可以單擊文章頂部或者底部的 討論訪問論壇。)

    不管正在構(gòu)建的是 J2EE 還是 J2SE 服務(wù)器應(yīng)用程序,都有可能以某種方式使用 Java Servlet —— 可能是直接地通過像 JSP 技術(shù)、Velocity 或者 WebMacro 這樣的表示層,也可能通過一個(gè)基于 servlet 的 Web 服務(wù)實(shí)現(xiàn),如 Axis 或者 Glue。Servlet API 提供的一個(gè)最重要的功能是會(huì)話管理 —— 通過 HttpSession 接口進(jìn)行用戶狀態(tài)的認(rèn)證、失效和維護(hù)。

    會(huì)話狀態(tài)

    幾乎每一個(gè) Web 應(yīng)用程序都有一些會(huì)話狀態(tài),這些狀態(tài)有可能像記住您是否已登錄這么簡(jiǎn)單,也可能是您的會(huì)話的更詳細(xì)的歷史,如購(gòu)物車的內(nèi)容、以前查詢結(jié)果的緩存或者 20 頁(yè)動(dòng)態(tài)問卷表的完整響應(yīng)歷史。因?yàn)?HTTP 協(xié)議本身是無狀態(tài)的,所以需要將會(huì)話狀態(tài)存儲(chǔ)在某處并與瀏覽會(huì)話以某種方式相關(guān)聯(lián),使得下次請(qǐng)求同一 Web 應(yīng)用程序的頁(yè)面時(shí)可以容易地獲取。幸運(yùn)的是,J2EE 提供了幾種管理會(huì)話狀態(tài)的方法 —— 狀態(tài)可以存儲(chǔ)在數(shù)據(jù)層,用 Servlet API 的 HttpSession 接口存儲(chǔ)在 Web 層,用有狀態(tài)會(huì)話 bean 存儲(chǔ)在 Enterprise JavaBeans(EJB)層,甚至用 cookie 或者隱藏表單字段將狀態(tài)存儲(chǔ)在客戶層。不幸的是,會(huì)話狀態(tài)管理不當(dāng)會(huì)帶來嚴(yán)重的性能問題。

    如果應(yīng)用程序能夠在 HttpSession 中存儲(chǔ)用戶狀態(tài),這種方法通常比其他方法更好。在客戶端用 HTTP cookie 或者隱藏表單字段存儲(chǔ)會(huì)話狀態(tài)有很大的安全風(fēng)險(xiǎn) —— 它將應(yīng)用程序的一部分內(nèi)部?jī)?nèi)容暴露給了非受信任的客戶層。(一個(gè)早期的電子商務(wù)網(wǎng)站將購(gòu)物車內(nèi)容(包括價(jià)格)存儲(chǔ)在隱藏表單字段中,從而可以很容易被非法利用,讓任何了解 HTML 和 HTTP 的用戶可以以 0.01 美元購(gòu)買任何商品。噢)此外,使用 cookie 或者隱藏表單字段很混亂,容易出錯(cuò),并且脆弱(如果用戶禁止在瀏覽器中使用 cookie,那么基于 cookie 的方法就完全不能工作)。

    在 J2EE 應(yīng)用程序中存儲(chǔ)服務(wù)器端狀態(tài)的其他方法是使用有狀態(tài)會(huì)話 bean,或者在數(shù)據(jù)庫(kù)中存儲(chǔ)會(huì)話狀態(tài)。雖然有狀態(tài)會(huì)話 bean 在會(huì)話狀態(tài)管理方面有更大的靈活性,但是在可能的情況下,將會(huì)話狀態(tài)存儲(chǔ)在 Web 層仍然有好處。如果業(yè)務(wù)對(duì)象是無狀態(tài)的,那么通常可以僅僅添加更多 Web 服務(wù)器來擴(kuò)展應(yīng)用程序,而不用添加更多 Web 服務(wù)器和更多 EJB 容器, 這樣的成本一般要低一些并且容易完成。使用 HttpSession 存儲(chǔ)會(huì)話狀態(tài)的另一個(gè)好處是 Servlet API 提供了一種會(huì)話失效時(shí)通知的容易方法。在數(shù)據(jù)庫(kù)中存儲(chǔ)會(huì)話狀態(tài)的成本可能難以承受。

    servlet 規(guī)范沒有要求 servlet 容器進(jìn)行某種類型的會(huì)話復(fù)制或者持久性,但是它建議將狀態(tài)復(fù)制作為 servlet 首要 存在理由(raison d'etre) 的重要部分,并且它對(duì)作為進(jìn)行會(huì)話復(fù)制的容器提出了一些要求。會(huì)話復(fù)制可以提供大量好處 —— 負(fù)載平衡、伸縮性、容錯(cuò)和高可用性。相應(yīng)地,大多數(shù) servlet 容器支持某種形式的 HttpSession 復(fù)制,但是復(fù)制的機(jī)制、配置和時(shí)間是由實(shí)現(xiàn)決定的。





    回頁(yè)首


    HttpSession API

    簡(jiǎn)單地說, HttpSession 接口支持幾種方法,servlet、JSP 頁(yè)或者其他表示層組件可以用這些方法來跨多個(gè) HTTP 請(qǐng)求維護(hù)會(huì)話信息。會(huì)話綁定到特定的用戶,但是在 Web 應(yīng)用程序的所有 servlet 中共享 —— 不特定于某一個(gè) servlet。一種考慮會(huì)話的有用方法是,會(huì)話像一個(gè)在會(huì)話期間存儲(chǔ)對(duì)象的 Map —— 可以用 setAttribute 按名字存儲(chǔ)會(huì)話屬性,并用 getAttribute 提取它們。 HttpSession 接口還包含會(huì)話生存周期方法,如 invalidate() (它通知容器應(yīng)丟棄會(huì)話)。清單 1 顯示 HttpSession 接口最常用的元素:


    清單 1. HttpSession API
    												
    														public interface HttpSession {
        Object getAttribute(String s);
        Enumeration getAttributeNames();
        void setAttribute(String s, Object o);
        void removeAttribute(String s);
    
        boolean isNew();
        void invalidate();
        void setMaxInactiveInterval(int i);
        int getMaxInactiveInterval();
        ...
    }
    
    												
    										

    理論上,可以跨群集一致性地完全復(fù)制會(huì)話狀態(tài),這樣群集中的所有節(jié)點(diǎn)都可以服務(wù)任何請(qǐng)求,一個(gè)簡(jiǎn)單的負(fù)載平衡器可以以輪詢方式傳送請(qǐng)求,避開有故障的主機(jī)。不過,這種緊密的復(fù)制有很高的性能成本,并且難于實(shí)現(xiàn),當(dāng)群集接近某一規(guī)模時(shí),還會(huì)有伸縮性的問題。

    一種更常用的方式是將負(fù)載平衡與會(huì)話相似性(affinity) 結(jié)合起來 —— 負(fù)載平衡器可以將會(huì)話與連接相關(guān)聯(lián),并將會(huì)話中以后的請(qǐng)求發(fā)送給同一服務(wù)器。有很多硬件和軟件負(fù)載平衡器支持這個(gè)功能,并且這意味著只有主連接主機(jī)和會(huì)話需要故障轉(zhuǎn)移到另一臺(tái)服務(wù)器時(shí)才訪問復(fù)制的會(huì)話信息。





    回頁(yè)首


    復(fù)制方式

    復(fù)制提供了一些可能的好處,包括可用性、容錯(cuò)和伸縮性。此外,有大量會(huì)話復(fù)制的方法可用:方法的選擇取決于應(yīng)用程序群集的規(guī)模、復(fù)制的目標(biāo)和 servlet 容器支持的復(fù)制設(shè)施。復(fù)制有性能成本,包括 CPU 周期(存儲(chǔ)在會(huì)話中的序列化對(duì)象)、網(wǎng)絡(luò)帶寬(廣播更新),以及基于磁盤的方案中寫入到磁盤或者數(shù)據(jù)庫(kù)的成本。

    幾乎所有 servlet 容器都通過存儲(chǔ)在 HttpSession 中的序列化對(duì)象進(jìn)行 HttpSession 復(fù)制,所以如果是創(chuàng)建一個(gè)分布式應(yīng)用程序,應(yīng)當(dāng)確保只將可序列化對(duì)象放到會(huì)話中。(一些容器對(duì)像 EJB 引用、事務(wù)上下文、還有其他非可序列化的 J2EE 對(duì)象類型有特殊的處理。)

    基于 JDBC 的復(fù)制

    一種會(huì)話復(fù)制的方法是序列化會(huì)話內(nèi)容并將它寫入數(shù)據(jù)庫(kù)。這種方法相當(dāng)直觀,其優(yōu)點(diǎn)是不僅會(huì)話可以故障轉(zhuǎn)移到其他主機(jī),而且即使整個(gè)群集失效,會(huì)話數(shù)據(jù)也可以保存下來。基于數(shù)據(jù)庫(kù)的復(fù)制的缺點(diǎn)是性能成本 —— 數(shù)據(jù)庫(kù)事務(wù)是昂貴的。雖然它可以在 Web 層很好地伸縮,但是它可能在數(shù)據(jù)層產(chǎn)生伸縮問題 —— 如果群集增長(zhǎng)大到一定程度,擴(kuò)展數(shù)據(jù)層以容納會(huì)話數(shù)據(jù)會(huì)很困難或者成本無法接受。

    基于文件的復(fù)制

    基于文件的復(fù)制類似于使用數(shù)據(jù)庫(kù)存儲(chǔ)序列化的會(huì)話,只不過是使用共享文件服務(wù)器而不是數(shù)據(jù)庫(kù)來存儲(chǔ)會(huì)話數(shù)據(jù)。這種方式的成本一般比使用數(shù)據(jù)庫(kù)的成本(硬件成本、軟件許可證和計(jì)算開銷)低,其代價(jià)則是可靠性(數(shù)據(jù)庫(kù)可提供比文件系統(tǒng)更強(qiáng)的持久化保證)。

    基于內(nèi)存的復(fù)制

    另一種復(fù)制方式是與群集中的一個(gè)或者多個(gè)其他服務(wù)器共享序列化的會(huì)話數(shù)據(jù)副本。復(fù)制所有會(huì)話到所有主機(jī)中提供了最大的可用性,并且負(fù)載平衡最容易,但是因?yàn)閺?fù)制消息所消耗的每個(gè)節(jié)點(diǎn)的內(nèi)存和網(wǎng)絡(luò)帶寬,最終會(huì)限制群集的規(guī)模。一些應(yīng)用服務(wù)器支持與“伙伴(buddy)”節(jié)點(diǎn)的基于內(nèi)存的復(fù)制,其中每一個(gè)會(huì)話存在于主服務(wù)器上和一臺(tái)(或更多)備份服務(wù)器上。這種方案比將所有會(huì)話復(fù)制到所有服務(wù)器的伸縮性更好,但是當(dāng)需要將會(huì)話故障轉(zhuǎn)移到另一臺(tái)服務(wù)器上時(shí)會(huì)使負(fù)載平衡任務(wù)復(fù)雜化,因?yàn)樗仨氄页隽硗饽囊慌_(tái)(幾臺(tái))服務(wù)器有這個(gè)會(huì)話。

    時(shí)間考慮

    除了決定如何存儲(chǔ)復(fù)制會(huì)話數(shù)據(jù),還有什么時(shí)候復(fù)制數(shù)據(jù)的問題。最可靠但也最昂貴的方法是每次數(shù)據(jù)改變時(shí)復(fù)制它(如每次 servlet 調(diào)用結(jié)束)。不那么昂貴、但是在故障時(shí)會(huì)有丟失一些數(shù)據(jù)的風(fēng)險(xiǎn)的方法是在每超過 N 秒時(shí)復(fù)制數(shù)據(jù)。

    與時(shí)間問題有關(guān)的問題是,是復(fù)制整個(gè)會(huì)話還是只試嘗復(fù)制會(huì)話中改變了的屬性(它包含的數(shù)據(jù)會(huì)少得多)。這些都需要在可靠性和性能之間進(jìn)行取舍。Servlet 開發(fā)人員應(yīng)當(dāng)認(rèn)識(shí)到在故障轉(zhuǎn)移時(shí),會(huì)話狀態(tài)可能變得“過時(shí)”(是幾次請(qǐng)求前的復(fù)制),并應(yīng)當(dāng)準(zhǔn)備處理不是最新的會(huì)話內(nèi)容。(例如,如果一個(gè)interview 的第 3 步產(chǎn)生一個(gè)會(huì)話屬性,而用戶在第 4 步時(shí),請(qǐng)求被故障轉(zhuǎn)移到一個(gè)具有兩次請(qǐng)求之前的會(huì)話狀態(tài)復(fù)制的系統(tǒng)上,那么第 4 步的 servlet 代碼應(yīng)預(yù)備在會(huì)話中找不到這個(gè)屬性,并采取相應(yīng)的行動(dòng) —— 如重定向,而不是認(rèn)定它會(huì)在那里、并在找不到它時(shí)拋出一個(gè) NullPointerException 。)





    回頁(yè)首


    容器支持

    Servlet 容器的 HttpSession 復(fù)制選項(xiàng)以及如何配置這些選項(xiàng)是各不相同的。IBM WebSphere ?提供的復(fù)制選項(xiàng)是最多的,它提供了在內(nèi)存中復(fù)制或者基于數(shù)據(jù)庫(kù)的復(fù)制、在 servlet 末尾或者基于時(shí)間的復(fù)制時(shí)間、傳播全部會(huì)話快照(JBoss 3.2 或以后版本)或者只傳播改變了的屬性等選擇。基于內(nèi)存的復(fù)制基于 JMS 發(fā)布-訂閱,它可以復(fù)制到所有克隆、一個(gè)“伙伴”復(fù)制品或者一個(gè)專門的復(fù)制服務(wù)器。

    WebLogic 還提供了一組選擇,包括內(nèi)存中(使用一個(gè)伙伴復(fù)制品)、基于文件的或者基于數(shù)據(jù)庫(kù)的。JBoss 與 Tomcat 或者 Jetty servlet 容器一同使用時(shí),進(jìn)行基于內(nèi)存的復(fù)制,可以選擇 servlet 末尾或者基于時(shí)間的復(fù)制時(shí)間,而快照選項(xiàng)(在 JBoss 3.2 或以后版本)是只復(fù)制改變了的屬性。Tomcat 5.0 為所有群集節(jié)點(diǎn)提供了基于內(nèi)存的復(fù)制。此外,通過像 WADI 這樣的項(xiàng)目,可以用 servlet 過濾機(jī)制將會(huì)話復(fù)制添加到像 Tomcat 或者 Jetty 這樣的 servlet 容器中。





    回頁(yè)首


    改進(jìn)分布式 Web 應(yīng)用程序的性能

    不管決定使用什么機(jī)制進(jìn)行會(huì)話復(fù)制,可以用幾種方式改進(jìn) Web 應(yīng)用程序的性能和伸縮性。首先記住,為了獲得會(huì)話復(fù)制的好處,需要在部署描述符中將 Web 應(yīng)用程序標(biāo)記為 distributable,并保證在會(huì)話中的所有內(nèi)容都是可序列化的。

    保持會(huì)話最小

    因?yàn)閺?fù)制會(huì)話有隨著會(huì)話中的對(duì)象圖(object graph) 的變大而增加成本,所以應(yīng)當(dāng)盡可能地在會(huì)話中少放置數(shù)據(jù)。這樣做會(huì)減少?gòu)?fù)制的序列化的開銷、網(wǎng)絡(luò)帶寬要求和磁盤要求。特別地,將共享對(duì)象存儲(chǔ)在會(huì)話中一般不是好主意,因?yàn)樗鼈冃枰獜?fù)制到它們所屬的 每一個(gè)會(huì)話中。

    不要繞過 setAttribute

    在改變會(huì)話的屬性時(shí),要知道即使 servlet 容器只是試圖做最小的更新(只傳播改變了的屬性),如果沒有調(diào)用 setAttribute ,容器也可能沒有注意到已經(jīng)改變的屬性。(想像在會(huì)話中有一個(gè) Vector ,表示購(gòu)物車中的商品 —— 如果調(diào)用 getAttribute() 獲取 Vector 、然后向它添加一些內(nèi)容,并且不再次調(diào)用 setAttribute ,容器可能不會(huì)意識(shí)到 Vector 已經(jīng)改變了。)

    使用細(xì)化的會(huì)話屬性

    對(duì)于支持最小更新的容器,可以通過將多個(gè)細(xì)化的對(duì)象而不是一個(gè)大塊頭放到會(huì)話中而降低會(huì)話復(fù)制的成本。這樣,對(duì)快速改變的數(shù)據(jù)的改變也不會(huì)迫使容器去序列化并傳播慢速改變的數(shù)據(jù)。

    完成后使之失效

    如果知道用戶完成了會(huì)話的使用(如,用戶選擇注銷登錄),確保調(diào)用 HttpSession.invalidate() 。否則,會(huì)話將持久化直到它失效,這會(huì)消耗內(nèi)存,并且可能是長(zhǎng)時(shí)間的(取決于會(huì)話超時(shí)時(shí)間)。許多 servlet 容器對(duì)可以跨所有會(huì)話使用的內(nèi)存的數(shù)量有一個(gè)限制,達(dá)到這個(gè)限制時(shí),會(huì)序列化最先使用的會(huì)話并將它寫到磁盤上。如果知道用戶使用完了會(huì)話,可以使容器不再處理它并使它作廢。

    保持會(huì)話干凈

    如果在會(huì)話中有大的項(xiàng),并且只在會(huì)話的一部分中使用,那么當(dāng)不再需要時(shí)應(yīng)刪除它們。刪除它們會(huì)減少會(huì)話復(fù)制的成本。(這種做法類似于使用顯式 nulling 以幫助垃圾收集器,老讀者知道我一般不建議這樣做,但是在這種情況下,因?yàn)橛袕?fù)制,在會(huì)話中保持垃圾的成本要高得多,因此值得以這種方式幫助容器。)





    回頁(yè)首


    結(jié)束語(yǔ)

    通過 HttpSession 復(fù)制,Servlet 容器可以在構(gòu)建復(fù)制的、高可用性的 Web 應(yīng)用程序方面給您減輕很多負(fù)擔(dān)。不過,對(duì)于復(fù)制有一些配置選項(xiàng),每個(gè)容器都不一樣,復(fù)制策略的選擇對(duì)于應(yīng)用程序的容錯(cuò)、性能和伸縮性有影響。復(fù)制策略的選擇不應(yīng)當(dāng)是事后的 —— 您應(yīng)當(dāng)在構(gòu)建 Web 應(yīng)用程序時(shí)就考慮它。并且,一定不要忘記進(jìn)行負(fù)載測(cè)試以確定應(yīng)用程序的伸縮性 —— 在客戶替您做之前。

    posted @ 2006-08-24 17:41 Binary 閱讀(218) | 評(píng)論 (0)編輯 收藏

    Java 理論與實(shí)踐: 關(guān)于異常的爭(zhēng)論

    關(guān)于在 Java 語(yǔ)言中使用異常的大多數(shù)建議都認(rèn)為,在確信異常可以被捕獲的任何情況下,應(yīng)該優(yōu)先使用檢查型異常。語(yǔ)言設(shè)計(jì)(編譯器強(qiáng)制您在方法簽名中列出可能被拋出的所有檢查型異常)以及早期關(guān)于樣式和用法的著作都支持該建議。最近,幾位著名的作者已經(jīng)開始認(rèn)為非檢查型異常在優(yōu)秀的 Java 類設(shè)計(jì)中有著比以前所認(rèn)為的更為重要的地位。在本文中,Brian Goetz 考察了關(guān)于使用非檢查型異常的優(yōu)缺點(diǎn)。請(qǐng)?jiān)诟綆У挠懻撜搲信c作者和其他讀者一起分享您有關(guān)本文的心得體會(huì)(您也可以點(diǎn)擊文章頂部或底部的 討論來訪問該論壇。)

    與 C++ 類似,Java 語(yǔ)言也提供異常的拋出和捕獲。但是,與 C++ 不一樣的是,Java 語(yǔ)言支持檢查型和非檢查型異常。Java 類必須在方法簽名中聲明它們所拋出的任何檢查型異常,并且對(duì)于任何方法,如果它調(diào)用的方法拋出一個(gè)類型為 E 的檢查型異常,那么它必須捕獲 E 或者也聲明為拋出 E(或者 E 的一個(gè)父類)。通過這種方式,該語(yǔ)言強(qiáng)制我們文檔化控制可能退出一個(gè)方法的所有預(yù)期方式。

    對(duì)于因?yàn)榫幊体e(cuò)誤而導(dǎo)致的異常,或者是不能期望程序捕獲的異常(解除引用一個(gè)空指針,數(shù)組越界,除零,等等),為了使開發(fā)人員免于處理這些異常,一些異常被命名為非檢查型異常(即那些繼承自 RuntimeException 的異常)并且不需要進(jìn)行聲明。

    傳統(tǒng)的觀點(diǎn)

    在下面的來自 Sun 的“The Java Tutorial”的摘錄中,總結(jié)了關(guān)于將一個(gè)異常聲明為檢查型還是非檢查型的傳統(tǒng)觀點(diǎn)(更多的信息請(qǐng)參閱 參考資料):

    因?yàn)?Java 語(yǔ)言并不要求方法捕獲或者指定運(yùn)行時(shí)異常,因此編寫只拋出運(yùn)行時(shí)異常的代碼或者使得他們的所有異常子類都繼承自 RuntimeException ,對(duì)于程序員來說是有吸引力的。這些編程捷徑都允許程序員編寫 Java 代碼而不會(huì)受到來自編譯器的所有挑剔性錯(cuò)誤的干擾,并且不用去指定或者捕獲任何異常。盡管對(duì)于程序員來說這似乎比較方便,但是它回避了 Java 的捕獲或者指定要求的意圖,并且對(duì)于那些使用您提供的類的程序員可能會(huì)導(dǎo)致問題。

    檢查型異常代表關(guān)于一個(gè)合法指定的請(qǐng)求的操作的有用信息,調(diào)用者可能已經(jīng)對(duì)該操作沒有控制,并且調(diào)用者需要得到有關(guān)的通知 —— 例如,文件系統(tǒng)已滿,或者遠(yuǎn)端已經(jīng)關(guān)閉連接,或者訪問權(quán)限不允許該動(dòng)作。

    如果您僅僅是因?yàn)椴幌胫付ó惓6鴴伋鲆粋€(gè) RuntimeException ,或者創(chuàng)建 RuntimeException 的一個(gè)子類,那么您換取到了什么呢?您只是獲得了拋出一個(gè)異常而不用您指定這樣做的能力。換句話說,這是一種用于避免文檔化方法所能拋出的異常的方式。在什么時(shí)候這是有益的?也就是說,在什么時(shí)候避免注明一個(gè)方法的行為是有益的?答案是“幾乎從不。”

    換句話說,Sun 告訴我們檢查型異常應(yīng)該是準(zhǔn)則。該教程通過多種方式繼續(xù)說明,通常應(yīng)該拋出異常,而不是 RuntimeException —— 除非您是 JVM。

    Effective Java: Programming Language Guide一書中(請(qǐng)參閱 參考資料),Josh Bloch 提供了下列關(guān)于檢查型和非檢查型異常的知識(shí)點(diǎn),這些與 “The Java Tutorial” 中的建議相一致(但是并不完全嚴(yán)格一致):

    • 第 39 條:只為異常條件使用異常。也就是說,不要為控制流使用異常,比如,在調(diào)用 Iterator.next() 時(shí)而不是在第一次檢查 Iterator.hasNext() 時(shí)捕獲 NoSuchElementException

    • 第 40 條:為可恢復(fù)的條件使用檢查型異常,為編程錯(cuò)誤使用運(yùn)行時(shí)異常。這里,Bloch 回應(yīng)傳統(tǒng)的 Sun 觀點(diǎn) —— 運(yùn)行時(shí)異常應(yīng)該只是用于指示編程錯(cuò)誤,例如違反前置條件。

    • 第 41 條:避免不必要的使用檢查型異常。換句話說,對(duì)于調(diào)用者不可能從其中恢復(fù)的情形,或者惟一可以預(yù)見的響應(yīng)將是程序退出,則不要使用檢查型異常。

    • 第 43 條:拋出與抽象相適應(yīng)的異常。換句話說,一個(gè)方法所拋出的異常應(yīng)該在一個(gè)抽象層次上定義,該抽象層次與該方法做什么相一致,而不一定與方法的底層實(shí)現(xiàn)細(xì)節(jié)相一致。例如,一個(gè)從文件、數(shù)據(jù)庫(kù)或者 JNDI 裝載資源的方法在不能找到資源時(shí),應(yīng)該拋出某種 ResourceNotFound 異常(通常使用異常鏈來保存隱含的原因),而不是更底層的 IOExceptionSQLException 或者 NamingException




    回頁(yè)首


    重新考察非檢查型異常的正統(tǒng)觀點(diǎn)

    最近,幾位受尊敬的專家,包括 Bruce Eckel 和 Rod Johnson,已經(jīng)公開聲明盡管他們最初完全同意檢查型異常的正統(tǒng)觀點(diǎn),但是他們已經(jīng)認(rèn)定排他性使用檢查型異常的想法并沒有最初看起來那樣好,并且對(duì)于許多大型項(xiàng)目,檢查型異常已經(jīng)成為一個(gè)重要的問題來源。Eckel 提出了一個(gè)更為極端的觀點(diǎn),建議所有的異常應(yīng)該是非檢查型的;Johnson 的觀點(diǎn)要保守一些,但是仍然暗示傳統(tǒng)的優(yōu)先選擇檢查型異常是過分的。(值得一提的是,C# 的設(shè)計(jì)師在語(yǔ)言設(shè)計(jì)中選擇忽略檢查型異常,使得所有異常都是非檢查型的,因而幾乎可以肯定他們具有豐富的 Java 技術(shù)使用經(jīng)驗(yàn)。但是,后來他們的確為檢查型異常的實(shí)現(xiàn)留出了空間。)





    回頁(yè)首


    對(duì)于檢查型異常的一些批評(píng)

    Eckel 和 Johnson 都指出了一個(gè)關(guān)于檢查型異常的相似的問題清單;一些是檢查型異常的內(nèi)在屬性,一些是檢查型異常在 Java 語(yǔ)言中的特定實(shí)現(xiàn)的屬性,還有一些只是簡(jiǎn)單的觀察,主要是關(guān)于檢查型異常的廣泛的錯(cuò)誤使用是如何變?yōu)橐粋€(gè)嚴(yán)重的問題,從而導(dǎo)致該機(jī)制可能需要被重新考慮。

    檢查型異常不適當(dāng)?shù)乇┞秾?shí)現(xiàn)細(xì)節(jié)

    您已經(jīng)有多少次看見(或者編寫)一個(gè)拋出 SQLException 或者 IOException 的方法,即使它看起來與數(shù)據(jù)庫(kù)或者文件毫無關(guān)系呢?對(duì)于開發(fā)人員來說,在一個(gè)方法的最初實(shí)現(xiàn)中總結(jié)出可能拋出的所有異常并且將它們?cè)黾拥椒椒ǖ?throws 子句(許多 IDE 甚至幫助您執(zhí)行該任務(wù))是十分常見的。這種直接方法的一個(gè)問題是它違反了 Bloch 的 第 43 條 —— 被拋出的異常所位于的抽象層次與拋出它們的方法不一致。

    一個(gè)用于裝載用戶概要的方法,在找不到用戶時(shí)應(yīng)該拋出 NoSuchUserException ,而不是 SQLException —— 調(diào)用者可以很好地預(yù)料到用戶可能找不到,但是不知道如何處理 SQLException 。異常鏈可以用于拋出一個(gè)更為合適的異常而不用丟棄關(guān)于底層失敗的細(xì)節(jié)(例如棧跟蹤),允許抽象層將位于它們之上的分層同位于它們之下的分層的細(xì)節(jié)隔離開來,同時(shí)保留對(duì)于調(diào)試可能有用的信息。

    據(jù)說,諸如 JDBC 包的設(shè)計(jì)采取這樣一種方式,使得它難以避免該問題。在 JDBC 接口中的每個(gè)方法都拋出 SQLException ,但是在訪問一個(gè)數(shù)據(jù)庫(kù)的過程中可能會(huì)經(jīng)歷多種不同類型的問題,并且不同的方法可能易受不同錯(cuò)誤模式的影響。一個(gè) SQLException 可能指示一個(gè)系統(tǒng)級(jí)問題(不能連接到數(shù)據(jù)庫(kù))、邏輯問題(在結(jié)果集中沒有更多的行)或者特定數(shù)據(jù)的問題(您剛才試圖插入行的主鍵已經(jīng)存在或者違反實(shí)體完整性約束)。如果沒有犯不可原諒的嘗試分析消息正文的過失,調(diào)用者是不可能區(qū)分這些不同類型的 SQLException 的。( SQLException 的確支持用于獲取數(shù)據(jù)庫(kù)特定錯(cuò)誤代碼和 SQL 狀態(tài)變量的方法,但是在實(shí)踐中這些很少用于區(qū)分不同的數(shù)據(jù)庫(kù)錯(cuò)誤條件。)

    不穩(wěn)定的方法簽名

    不穩(wěn)定的方法簽名問題是與前面的問題相關(guān)的 —— 如果您只是通過一個(gè)方法傳遞異常,那么您不得不在每次改變方法的實(shí)現(xiàn)時(shí)改變它的方法簽名,以及改變調(diào)用該方法的所有代碼。一旦類已經(jīng)被部署到產(chǎn)品中,管理這些脆弱的方法簽名就變成一個(gè)昂貴的任務(wù)。然而,該問題本質(zhì)上是沒有遵循 Bloch 提出的第 43 條的另一個(gè)癥狀。方法在遇到失敗時(shí)應(yīng)該拋出一個(gè)異常,但是該異常應(yīng)該反映該方法做什么,而不是它如何做。

    有時(shí),當(dāng)程序員對(duì)因?yàn)閷?shí)現(xiàn)的改變而導(dǎo)致從方法簽名中增加或者刪除異常感到厭煩時(shí),他們不是通過使用一個(gè)抽象來定義特定層次可能拋出的異常類型,而只是將他們的所有方法都聲明為拋出 Exception 。換句話說,他們已經(jīng)認(rèn)定異常只是導(dǎo)致煩惱,并且基本上將它們關(guān)閉掉了。毋庸多言,該方法對(duì)于絕大多數(shù)可任意使用的代碼來說通常不是一個(gè)好的錯(cuò)誤處理策略。

    難以理解的代碼

    因?yàn)樵S多方法都拋出一定數(shù)目的不同異常,錯(cuò)誤處理的代碼相對(duì)于實(shí)際的功能代碼的比率可能會(huì)偏高,使得難以找到一個(gè)方法中實(shí)際完成功能的代碼。異常是通過集中錯(cuò)誤處理來設(shè)想減小代碼的,但是一個(gè)具有三行代碼和六個(gè) catch 塊(其中每個(gè)塊只是記錄異常或者包裝并重新拋出異常)的方法看起來比較膨脹并且會(huì)使得本來簡(jiǎn)單的代碼變得模糊。

    異常淹沒

    我們都看到過這樣的代碼,其中捕獲了一個(gè)異常,但是在 catch 塊中沒有代碼。盡管這種編程實(shí)踐很明顯是不好的,但是很容易看出它是如何發(fā)生的 —— 在原型化期間,某人通過 try...catch 塊包裝代碼,而后來忘記返回并填充 catch 塊。盡管這個(gè)錯(cuò)誤很常見,但是這也是更好的工具可以幫助我們的地方之一 —— 對(duì)于異常淹沒的地方,通過編輯器、編譯器或者靜態(tài)檢查工具可以容易地檢測(cè)并發(fā)出警告。

    極度通用的 try...catch 塊是另一種形式的異常淹沒,并且更加難以檢測(cè),因?yàn)檫@是 Java 類庫(kù)中的異常類層次的結(jié)構(gòu)而導(dǎo)致的(可疑)。讓我們假定一個(gè)方法拋出四個(gè)不同類型的異常,并且調(diào)用者遇到其中任何一個(gè)異常都將捕獲、記錄它們,并且返回。實(shí)現(xiàn)該策略的一種方式是使用一個(gè)帶有四個(gè) catch 子句的 try...catch 塊,其中每個(gè)異常類型一個(gè)。為了避免代碼難以理解的問題,一些開發(fā)人員將重構(gòu)該代碼,如清單 1 所示:


    清單 1. 意外地淹沒 RuntimeException
    												
    														try { 
      doSomething();
    }
    catch (Exception e) { 
      log(e);
    }
    
    												
    										

    盡管該代碼與四個(gè) catch 塊相比更為緊湊,但是它具有一個(gè)問題 —— 它還捕獲可能由 doSomething 拋出的任何 RuntimeException 并且阻止它們進(jìn)行擴(kuò)散。

    過多的異常包裝

    如果異常是在一個(gè)底層的設(shè)施中生成的,并且通過許多代碼層向上擴(kuò)散,在最終被處理之前它可能被捕獲、包裝和重新拋出若干次。當(dāng)異常最終被記錄的時(shí)候,棧跟蹤可能有許多頁(yè),因?yàn)闂8櫩赡鼙粡?fù)制多次,其中每個(gè)包裝層一次。(在 JDK 1.4 以及后來的版本中,異常鏈的實(shí)現(xiàn)在某種程度上緩解了該問題。)





    回頁(yè)首


    替換的方法

    Bruce Eckel, Thinking in Java(請(qǐng)參閱 參考資料)的作者,聲稱在使用 Java 語(yǔ)言多年后,他已經(jīng)得出這樣的結(jié)論,認(rèn)為檢查型異常是一個(gè)錯(cuò)誤 —— 一個(gè)應(yīng)該被聲明為失敗的試驗(yàn)。Eckel 提倡將所有的異常都作為非檢查型的,并且提供清單 2 中的類作為將檢查型異常轉(zhuǎn)變?yōu)榉菣z查型異常的一個(gè)方法,同時(shí)保留當(dāng)異常從棧向上擴(kuò)散時(shí)捕獲特定類型的異常的能力(關(guān)于如何使用該方法的解釋,請(qǐng)參閱他在 參考資料小節(jié)中的文章):


    清單 2. Eckel 的異常適配器類
    												
    														class ExceptionAdapter extends RuntimeException {
      private final String stackTrace;
      public Exception originalException;
      public ExceptionAdapter(Exception e) {
        super(e.toString());
        originalException = e;
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        stackTrace = sw.toString();
      }
      public void printStackTrace() { 
        printStackTrace(System.err);
      }
      public void printStackTrace(java.io.PrintStream s) { 
        synchronized(s) {
          s.print(getClass().getName() + ": ");
          s.print(stackTrace);
        }
      }
      public void printStackTrace(java.io.PrintWriter s) { 
        synchronized(s) {
          s.print(getClass().getName() + ": ");
          s.print(stackTrace);
        }
      }
      public void rethrow() { throw originalException; }
    } 
    
    												
    										

    如果查看 Eckel 的 Web 站點(diǎn)上的討論,您將會(huì)發(fā)現(xiàn)回應(yīng)者是嚴(yán)重分裂的。一些人認(rèn)為他的提議是荒謬的;一些人認(rèn)為這是一個(gè)重要的思想。(我的觀點(diǎn)是,盡管恰當(dāng)?shù)厥褂卯惓4_實(shí)是很難的,并且對(duì)異常用不好的例子大量存在,但是大多數(shù)贊同他的人是因?yàn)殄e(cuò)誤的原因才這樣做的,這與一個(gè)政客位于一個(gè)可以隨便獲取巧克力的平臺(tái)上參選將會(huì)獲得十歲孩子的大量選票的情況具有相似之處。)

    Rod Johnson 是 J2EE Design and Development(請(qǐng)參閱 參考資料) 的作者,這是我所讀過的關(guān)于 Java 開發(fā),J2EE 等方面的最好的書籍之一。他采取一個(gè)不太激進(jìn)的方法。他列舉了異常的多個(gè)類別,并且為每個(gè)類別確定一個(gè)策略。一些異常本質(zhì)上是次要的返回代碼(它通常指示違反業(yè)務(wù)規(guī)則),而一些異常則是“發(fā)生某種可怕錯(cuò)誤”(例如數(shù)據(jù)庫(kù)連接失敗)的變種。Johnson 提倡對(duì)于第一種類別的異常(可選的返回代碼)使用檢查型異常,而對(duì)于后者使用運(yùn)行時(shí)異常。在“發(fā)生某種可怕錯(cuò)誤”的類別中,其動(dòng)機(jī)是簡(jiǎn)單地認(rèn)識(shí)到?jīng)]有調(diào)用者能夠有效地處理該異常,因此它也可能以各種方式沿著棧向上擴(kuò)散而對(duì)于中間代碼的影響保持最小(并且最小化異常淹沒的可能性)。

    Johnson 還列舉了一個(gè)中間情形,對(duì)此他提出一個(gè)問題,“只是少數(shù)調(diào)用者希望處理問題嗎?”對(duì)于這些情形,他也建議使用非檢查型異常。作為該類別的一個(gè)例子,他列舉了 JDO 異常 —— 大多數(shù)情況下,JDO 異常表示的情況是調(diào)用者不希望處理的,但是在某些情況下,捕獲和處理特定類型的異常是有用的。他建議在這里使用非檢查型異常,而不是讓其余的使用 JDO 的類通過捕獲和重新拋出這些異常的形式來彌補(bǔ)這個(gè)可能性。

    使用非檢查型異常

    關(guān)于是否使用非檢查型異常的決定是復(fù)雜的,并且很顯然沒有明顯的答案。Sun 的建議是對(duì)于任何情況使用它們,而 C# 方法(也就是 Eckel 和其他人所贊同的)是對(duì)于任何情況都不使用它們。其他人說,“還存在一個(gè)中間情形。”

    通過在 C++ 中使用異常,其中所有的異常都是非檢查型的,我已經(jīng)發(fā)現(xiàn)非檢查型異常的最大風(fēng)險(xiǎn)之一就是它并沒有按照檢查型異常采用的方式那樣自我文檔化。除非 API 的創(chuàng)建者明確地文檔化將要拋出的異常,否則調(diào)用者沒有辦法知道在他們的代碼中將要捕獲的異常是什么。不幸的是,我的經(jīng)驗(yàn)是大多數(shù) C++ API 的文檔化非常差,并且即使文檔化很好的 API 也缺乏關(guān)于從一個(gè)給定方法可能拋出的異常的足夠信息。我看不出有任何理由可以說該問題對(duì)于 Java 類庫(kù)不是同樣的常見,因?yàn)?Jav 類庫(kù)嚴(yán)重依賴于非檢查型異常。依賴于您自己的或者您的合作伙伴的編程技巧是非常困難的;如果不得不依賴于某個(gè)人的文檔化技巧,那么對(duì)于他的代碼您可能得使用調(diào)用棧中的十六個(gè)幀來作為您的主要的錯(cuò)誤處理機(jī)制,這將會(huì)是令人恐慌的。

    文檔化問題進(jìn)一步強(qiáng)調(diào)為什么懶惰是導(dǎo)致選擇使用非檢查型異常的一個(gè)不好的原因,因?yàn)閷?duì)于文檔化增加給包的負(fù)擔(dān),使用非檢查型異常應(yīng)該比使用檢查型異常甚至更高(當(dāng)文檔化您所拋出的非檢查型異常比檢查型異常變得更為重要的時(shí)候)。





    回頁(yè)首


    文檔化,文檔化,文檔化

    如果決定使用非檢查型異常,您需要徹底地文檔化這個(gè)選擇,包括在 Javadoc 中文檔化一個(gè)方法可能拋出的所有非檢查型異常。Johnson 建議在每個(gè)包的基礎(chǔ)上選擇檢查型和非檢查型異常。使用非檢查型異常時(shí)還要記住,即使您并不捕獲任何異常,也可能需要使用 try...finally 塊,從而可以執(zhí)行清除動(dòng)作例如關(guān)閉數(shù)據(jù)庫(kù)連接。對(duì)于檢查型異常,我們有 try...catch 用來提示增加一個(gè) finally 子句。對(duì)于非檢查型異常,我們則沒有這個(gè)支撐可以依靠。

    posted @ 2006-08-24 17:39 Binary 閱讀(198) | 評(píng)論 (0)編輯 收藏

    Java 理論和實(shí)踐: 理解 JTS ― 平衡安全性和性能

    為 EJB 組件定義事務(wù)劃分和隔離屬性(attribute)的職責(zé)由應(yīng)用程序裝配人員來承擔(dān)。如果這些屬性設(shè)置不當(dāng),會(huì)對(duì)應(yīng)用程序的性能、可伸縮性或容錯(cuò)能力造成嚴(yán)重的后果。不幸的是,并沒有一種必須遵守的規(guī)則用于正確設(shè)置這些屬性,但有一些指導(dǎo)可以幫助我們?cè)诓l(fā)危險(xiǎn)和性能危險(xiǎn)之間找到一種平衡。

    我們?cè)诘?1 部分中討論過,事務(wù)主要是一種異常處理機(jī)制。事務(wù)在程序中的用途與合法合同在日常業(yè)務(wù)中的用途相似:如果出了什么問題它們可以幫助恢復(fù)。但由于大多數(shù)時(shí)間內(nèi)都沒實(shí)際 發(fā)生什么錯(cuò)誤,我們就希望能夠盡量減少它們的開銷以及對(duì)其余時(shí)間的占用。我們?cè)趹?yīng)用程序中如何使用事務(wù)會(huì)對(duì)應(yīng)用程序的性能和可伸縮性產(chǎn)生很大的影響。

    事務(wù)劃分

    J2EE 容器提供了兩種機(jī)制用來定義事務(wù)的起點(diǎn)和終點(diǎn):bean 管理的事務(wù)和容器管理的事務(wù)。在 bean 管理的事務(wù)中,用 UserTransaction.begin()UserTransaction.commit() 在 bean 方法中顯式開始和結(jié)束一個(gè)事務(wù)。另一方面,容器管理的事務(wù)提供了更多的靈活性。通過在裝配描述符中為每個(gè) EJB 方法定義事務(wù)性屬性,您可以指定每個(gè)方法的事務(wù)性需求并讓容器確定何時(shí)開始和結(jié)束一個(gè)事務(wù)。無論在哪種情況下,構(gòu)建事務(wù)的基本指導(dǎo)方針都是一樣的。

    進(jìn)來,出去

    事務(wù)劃分的第一條規(guī)則是“盡量短小”。事務(wù)提供并發(fā)控制;這通常意味著資源管理器將代表您獲得您在事務(wù)期間訪問的數(shù)據(jù)項(xiàng)的鎖,并且它必須一直持有這些鎖,直到事務(wù)結(jié)束。(請(qǐng)回憶一下本系列第 1 部分所討論的 ACID特性,其中“ACID”的“I”代表“隔離”(Isolation)。也就是說,一個(gè)事務(wù)的結(jié)果影響不到與該事務(wù)并發(fā)執(zhí)行的其它事務(wù)。)當(dāng)您擁有鎖時(shí),任何需要訪問您鎖定的數(shù)據(jù)項(xiàng)的其它事務(wù)將不得不一直等待,直到您釋放鎖。如果您的事務(wù)很長(zhǎng),那些其它的所有事務(wù)都將被鎖定,您的應(yīng)用程序吞吐量將大幅度下降。

    規(guī)則 1:使事務(wù)盡可能短小。

    通過使事務(wù)盡量短小,您可以把阻礙其它事務(wù)的時(shí)間縮到最短,從而提高應(yīng)用程序的可伸縮性。保持事務(wù)盡可能短小的最好方法當(dāng)然是不在事務(wù)中間做任何不必要耗費(fèi)時(shí)間的事,特別是不要在事務(wù)中間等待用戶輸入。

    開始一個(gè)事務(wù),從數(shù)據(jù)庫(kù)檢索一些數(shù)據(jù),顯示數(shù)據(jù),然后在仍處于事務(wù)中時(shí)請(qǐng)用戶做出一個(gè)選擇可能比較誘人。千萬別這么做!即使用戶注意力集中,也要花費(fèi)數(shù)秒來響應(yīng) ― 而在數(shù)據(jù)庫(kù)中擁有鎖數(shù)秒的時(shí)間已經(jīng)是很長(zhǎng)的了。如果用戶決定離開計(jì)算機(jī),或許是去吃午餐或者甚至回家一天,會(huì)發(fā)生什么情況?應(yīng)用程序?qū)⒅缓脽o奈停機(jī)。在事務(wù)期間執(zhí)行 I/O 是導(dǎo)致災(zāi)難的秘訣。

    規(guī)則 2:在事務(wù)期間不要等待用戶輸入。

    將相關(guān)的操作歸在一起

    由于每個(gè)事務(wù)都有不小的開銷,您可能認(rèn)為最好是在單個(gè)事務(wù)中執(zhí)行盡可能多的操作以使每個(gè)操作的開銷達(dá)到最小。但規(guī)則 1 告訴我們長(zhǎng)事務(wù)對(duì)可伸縮性不利。那么如何實(shí)現(xiàn)最小化每個(gè)操作的開銷和可伸縮性之間的平衡呢?

    我們把規(guī)則 1 設(shè)置為邏輯上的極端 ― 每個(gè)事務(wù)一個(gè)操作 ― 這樣不僅會(huì)導(dǎo)致額外開銷,還會(huì)危及應(yīng)用程序狀態(tài)的一致性。假定事務(wù)性資源管理器維護(hù)應(yīng)用程序狀態(tài)的一致性(請(qǐng)回憶一下第 1 部分,其中“ACID”的“C”代表“一致性”(Consistency)),但它們依賴應(yīng)用程序來定義一致性的意思。實(shí)際上,我們?cè)诿枋鍪聞?wù)時(shí)使用的一致性的定義有點(diǎn)圓滑:應(yīng)用程序說一致性是什么意思它就是什么意思。應(yīng)用程序把幾組應(yīng)用程序狀態(tài)的變化組織到幾個(gè)事務(wù)中,結(jié)果應(yīng)用程序的狀態(tài)就成了 定義上的(by definition)一致。然后資源管理器確保如果它必須從故障恢復(fù)的話,就把應(yīng)用程序狀態(tài)恢復(fù)到最近的一致狀態(tài)。

    在第 1 部分中,我們給出了一個(gè)在銀行應(yīng)用程序中將資金從一個(gè)帳戶轉(zhuǎn)移到另一個(gè)帳戶的示例。清單 1 展示了這個(gè)示例可能的 SQL 實(shí)現(xiàn),它包含 5 個(gè) SQL 操作(一個(gè)選擇,兩個(gè)更新和兩個(gè)插入操作):


    清單 1. 資金轉(zhuǎn)移的樣本 SQL 代碼
    												
    														SELECT accountBalance INTO aBalance 
        FROM Accounts WHERE accountId=aId;
    IF (aBalance >= transferAmount) THEN 
        UPDATE Accounts 
            SET accountBalance = accountBalance - transferAmount
            WHERE accountId = aId;
        UPDATE Accounts 
            SET accountBalance = accountBalance + transferAmount
            WHERE accountId = bId;
        INSERT INTO AccountJournal (accountId, amount)
            VALUES (aId, -transferAmount);
        INSERT INTO AccountJournal (accountId, amount)
            VALUES (bId, transferAmount);
    ELSE
        FAIL "Insufficient funds in account";
    END IF
    
    												
    										

    如果我們把這個(gè)操作作為五個(gè)單獨(dú)的事務(wù)來執(zhí)行會(huì)發(fā)生什么情況?這樣不僅會(huì)使執(zhí)行速度變慢(由于事務(wù)開銷),還會(huì)失去一致性。例如,如果一個(gè)人從帳戶 A 取了錢,作為執(zhí)行第一次 SELECT(檢查余額)和隨后的記入借方 UPDATE 之間的一個(gè)單獨(dú)事務(wù)的一部分,會(huì)發(fā)生什么情況?這樣會(huì)違反我們認(rèn)為這段代碼會(huì)強(qiáng)制遵守的業(yè)務(wù)規(guī)則 ― 帳戶余額應(yīng)該是非負(fù)的。如果在第一次 UPDATE 和第二次 UPDATE 之間系統(tǒng)失敗會(huì)發(fā)生什么情況?現(xiàn)在,當(dāng)系統(tǒng)恢復(fù)時(shí),錢已經(jīng)離開了帳戶 A 但還沒有記入帳戶 B 的貸方,并且也無記錄說明原因。這樣,哪個(gè)帳戶的所有者都不會(huì)開心。

    清單 1 中的五個(gè) SQL 操作是單個(gè)相關(guān)操作 ― 將資金從一個(gè)帳戶轉(zhuǎn)移到另一個(gè)帳戶 ― 的一部分。因此,我們希望要么全部執(zhí)行它們,要么一個(gè)也不執(zhí)行,建議在單個(gè)事務(wù)中全部執(zhí)行它們。

    規(guī)則 3:將相關(guān)操作歸到單個(gè)事務(wù)中。

    理想化的平衡

    規(guī)則 1 說事務(wù)應(yīng)盡可能短小。清單 1 中的示例表明有時(shí)候我們必須把一些操作歸到一個(gè)事務(wù)中來維護(hù)一致性。當(dāng)然,它要依賴應(yīng)用程序來確定“相關(guān)操作”是由什么組成的。我們可以把規(guī)則 1 和 3 結(jié)合在一起,提供一個(gè)描述事務(wù)范圍的一般指導(dǎo),我們規(guī)定它為規(guī)則 4:

    規(guī)則 4:把相關(guān)操作歸到單個(gè)事務(wù)中,但把不相關(guān)的操作放到單獨(dú)的事務(wù)中。





    回頁(yè)首


    容器管理的事務(wù)

    在使用容器管理的事務(wù)時(shí),不是顯式聲明事務(wù)的起點(diǎn)和終點(diǎn),而是為每個(gè) EJB 方法定義事務(wù)性需求。bean 的 assembly-descriptorcontainer-transaction 部分的 trans-attribute 元素中定義了事務(wù)模式。(清單 2 中顯示了一個(gè) assembly-descriptor 示例。)方法的事務(wù)模式以及狀態(tài) ― 調(diào)用方法是否早已在事務(wù)中被征用 ― 決定了當(dāng) EJB 方法被調(diào)用時(shí)容器應(yīng)該進(jìn)行下面幾個(gè)操作中的哪一個(gè):

    • 征用現(xiàn)有事務(wù)中的方法。
    • 創(chuàng)建一個(gè)新事務(wù),并征用該事務(wù)中的方法。
    • 不征用任何事務(wù)中的方法。
    • 拋出一個(gè)異常。

    清單 2. 樣本 EJB 裝配描述符
    												
    														<assembly-descriptor>
      ...
      <container-transaction>
        <method>
          <ejb-name>MyBean</ejb-name>
          <method-name>*</method-name>
        </method>
        <trans-attribute>Required</trans-attribute>
      </container-transaction>
      <container-transaction>
        <method>
          <ejb-name>MyBean</ejb-name>
          <method-name>logError</method-name>
        </method>
        <trans-attribute>RequiresNew</trans-attribute>
      </container-transaction>
      ...
    </assembly-descriptor>
    
    												
    										

    J2EE 規(guī)范定義了六種事務(wù)模式: RequiredRequiresNewMandatorySupportsNotSupportedNever 。表 1 概述了每種模式的行為 ― 在現(xiàn)有事務(wù)中被調(diào)用和不在事務(wù)內(nèi)調(diào)用時(shí)的行為 ― 并描述了每種模式受哪些類型的 EJB 組件支持。(一些容器可能允許您在選擇事務(wù)模式時(shí)有更多的靈活性,但這種使用要依賴特定于容器的功能,因此不適合跨容器的情況)。

    表 1. 事務(wù)模式

    事務(wù)模式 Bean 類型 在事務(wù) T 內(nèi)被調(diào)用時(shí)的行為 在事務(wù)外被調(diào)用時(shí)的行為
    Required 會(huì)話、實(shí)體、消息驅(qū)動(dòng) 在 T 中征用 新建事務(wù)
    RequiresNew 會(huì)話、實(shí)體 新建事務(wù) 新建事務(wù)
    Supports 會(huì)話、消息驅(qū)動(dòng) 在 T 中征用 不帶事務(wù)運(yùn)行
    Mandatory 會(huì)話、實(shí)體 在 T 中征用 出錯(cuò)
    NotSupported 會(huì)話、消息驅(qū)動(dòng) 不帶事務(wù)運(yùn)行 不帶事務(wù)運(yùn)行
    Never 會(huì)話、消息驅(qū)動(dòng) 出錯(cuò) 不帶事務(wù)運(yùn)行

    在只使用容器管理的事務(wù)的應(yīng)用程序中,只有組件調(diào)用事務(wù)模式為 RequiredRequiresNew 的 EJB 方法時(shí)才啟動(dòng)事務(wù)。如果容器創(chuàng)建一個(gè)事務(wù)作為調(diào)用事務(wù)性方法的結(jié)果,當(dāng)該方法完成時(shí)將關(guān)閉該事務(wù)。如果方法正常返回,容器將提交事務(wù)(除非應(yīng)用程序已經(jīng)要求回滾事務(wù))。如果方法通過拋出一個(gè)異常退出,容器將回滾事務(wù)并傳播該異常。如果在現(xiàn)有事務(wù) T 中調(diào)用了一個(gè)方法,并且事務(wù)模式指定應(yīng)該不帶事務(wù)運(yùn)行該方法或者在新事務(wù)中運(yùn)行該方法,那么事務(wù) T 將被暫掛,一直到方法完成,然后先前的事務(wù) T 被恢復(fù)。

    選擇一種事務(wù)模式

    那么我們應(yīng)該為自己的 bean 方法選擇哪種模式呢?對(duì)于會(huì)話 bean 和消息驅(qū)動(dòng) bean,您通常想使用 Required 來確保每個(gè)調(diào)用都被作為事務(wù)的一部分執(zhí)行,但仍將允許方法作為一個(gè)更大的事務(wù)的組件。請(qǐng)小心使用 RequiresNew ;只有在確定自己的方法的行為應(yīng)該與調(diào)用您的方法的行為分開提交時(shí),才應(yīng)該使用這種模式。 RequiresNew 一般情況下只和與系統(tǒng)中其它對(duì)象關(guān)系很少或沒什么關(guān)系的對(duì)象(比如日志對(duì)象)一起使用。(把 RequiresNew 與日志對(duì)象一起使用比較有意義,因?yàn)槟赡芟M诓还芡鈬聞?wù)是否提交的情況下提交日志消息。)

    RequiresNew 使用不當(dāng)會(huì)導(dǎo)致與上面的描述相似的情況,其中,清單 1 中的代碼在五個(gè)分開的事務(wù)而不是一個(gè)事務(wù)中執(zhí)行,這樣會(huì)使應(yīng)用程序處于不一致狀態(tài)。

    對(duì)于 CMP(容器管理的持久性,container-managed persistence)實(shí)體 bean,通常是希望使用 RequiredMandatory 也是一個(gè)合理的選項(xiàng),特別是在最初開發(fā)時(shí);這將會(huì)警告您實(shí)體 bean 方法在事務(wù)外被調(diào)用這種情況,這時(shí)可能會(huì)指出一個(gè)部署錯(cuò)誤。您幾乎從不希望把 RequiresNew 和 CMP 實(shí)體 bean 一起使用。 NotSupportedNever 旨在用于非事務(wù)性資源,比如 Java 事務(wù) API(Java Transaction API,JTA)事務(wù)中無法征用的外部非事務(wù)性系統(tǒng)或事務(wù)性系統(tǒng)的適配器。

    如果 EJB 應(yīng)用程序設(shè)計(jì)得當(dāng),應(yīng)用上面的事務(wù)模式指導(dǎo)往往會(huì)自然地產(chǎn)生規(guī)則 4 建議的事務(wù)劃分。原因是 J2EE 體系架構(gòu)鼓勵(lì)把應(yīng)用程序分解為最小的方便處理的塊,并且每個(gè)塊都作為一個(gè)單獨(dú)的請(qǐng)求被處理( 不管是以 HTTP 請(qǐng)求的形式還是作為在 JMS 隊(duì)列中排隊(duì)的消息的結(jié)果)。





    回頁(yè)首


    重溫隔離

    在第 1 部分中,我們定義了 隔離(isolation)的意思是:一個(gè)事務(wù)的影響對(duì)與該事務(wù)并發(fā)執(zhí)行的其它事務(wù)是不可見的;從事務(wù)的角度來看,好象事務(wù)是連續(xù)執(zhí)行而非并行執(zhí)行。盡管事務(wù)性資源管理器經(jīng)常可以同時(shí)處理許多事務(wù)并提供隔離的假象,但有時(shí)隔離限制實(shí)際上要求把新事務(wù)延遲到現(xiàn)有事務(wù)完成后才開始。由于完成一個(gè)事務(wù)至少包括一個(gè)同步磁盤 I/O(寫到事務(wù)日志),這就會(huì)把每秒的事務(wù)數(shù)限制到接近每秒的寫磁盤次數(shù),這對(duì)可伸縮性不利。

    實(shí)際上,通常是充分放松隔離需求以允許更多的事務(wù)并發(fā)執(zhí)行并使系統(tǒng)響應(yīng)能夠得到改善,使可伸縮性變得更強(qiáng)。幾乎所有的數(shù)據(jù)庫(kù)都支持標(biāo)準(zhǔn)隔離級(jí)別:讀未提交的(Read Uncommitted)、讀已提交的(Read Committed)、可重復(fù)的讀(Repeatable Read) 和可串行化的(Serializable)。

    不幸的是,為容器管理的事務(wù)管理隔離目前是在 J2EE 規(guī)范的范圍之外。但是,許多 J2EE 容器,比如 IBM WebSphere 和 BEA WebLogic,將提供特定于容器的擴(kuò)展,這些擴(kuò)展允許您以每方法(per-method)為基礎(chǔ)設(shè)置事務(wù)隔離級(jí)別,設(shè)置方法與在裝配描述符中設(shè)置事務(wù)模式的方法相同。對(duì)于 bean 管理的事務(wù),您可以通過 JDBC 或者其它資源管理器連接設(shè)置隔離級(jí)別。

    為闡明隔離級(jí)別之間的差異,我們首先把幾個(gè)并發(fā)危險(xiǎn)分類 ― 這幾種危險(xiǎn)是當(dāng)沒有適當(dāng)?shù)馗綦x時(shí)一個(gè)事務(wù)可能會(huì)干涉另一個(gè)事務(wù)的情況。下列的所有這些危險(xiǎn)都與這種情況( 第二個(gè)事務(wù)已經(jīng)啟動(dòng)后第一個(gè)事務(wù)變得對(duì)第二個(gè)事務(wù) 可見)的結(jié)果有關(guān):

    • 臟讀(Dirty Read):當(dāng)一個(gè)事務(wù)的中間(未提交的)結(jié)果對(duì)另一個(gè)事務(wù)可見時(shí)就會(huì)發(fā)生這種情況。
    • 不可重復(fù)的讀(Unrepeatable Read):當(dāng)一個(gè)事務(wù)讀取一個(gè)數(shù)據(jù)項(xiàng),然后重新讀取這個(gè)數(shù)據(jù)項(xiàng)并看到不同的值時(shí)就是發(fā)生了這種情況。
    • 虛讀(Phantom Read):當(dāng)一個(gè)事務(wù)執(zhí)行返回多個(gè)行的查詢,稍后再次執(zhí)行同一個(gè)查詢并看到第一次執(zhí)行該查詢沒出現(xiàn)的額外行時(shí)就是發(fā)生了這種情況。

    四個(gè)標(biāo)準(zhǔn)隔離級(jí)別與這三個(gè)隔離危險(xiǎn)相關(guān),如表 2 所示。最低的隔離級(jí)別“讀未提交的”并不能保護(hù)事務(wù)不被其它事務(wù)更改,但它的速度最快,因?yàn)樗恍枰獱?zhēng)奪讀鎖。最高的隔離級(jí)別“可串行化的”與上面給出的隔離的定義相當(dāng);每個(gè)事務(wù)好象都與其它事務(wù)的影響完全隔離。

    表 2. 事務(wù)隔離級(jí)別

    隔離級(jí)別 臟讀 不可重復(fù)的讀 虛讀
    讀未提交的
    讀已提交的
    可重復(fù)的讀
    可串行化的

    對(duì)于大多數(shù)數(shù)據(jù)庫(kù),缺省的隔離級(jí)別為“讀已提交的”,這是個(gè)很好的缺省選擇,因?yàn)樗柚故聞?wù)在事務(wù)中的任何給定的點(diǎn)看到應(yīng)用程序數(shù)據(jù)的不一致視圖。“讀已提交的”是一個(gè)很不錯(cuò)的隔離級(jí)別,用于大多數(shù)典型的短事務(wù),比如獲取報(bào)表數(shù)據(jù)或獲取要顯示給用戶的數(shù)據(jù)的時(shí)候(多半是作為 Web 請(qǐng)求的結(jié)果),也用于將新數(shù)據(jù)插入到數(shù)據(jù)庫(kù)的情況。

    當(dāng)您需要所有事務(wù)間有較高級(jí)別的一致性時(shí),使用較高的隔離級(jí)別“可重復(fù)的讀”和“可串行化的”比較合適,比如在清單 1 示例中,您希望從檢查余額以確保有足夠的資金到您實(shí)際取錢期間賬戶余額一直保持不變;這就要求至少要用“可重復(fù)的讀”隔離級(jí)別。在數(shù)據(jù)一致性絕對(duì)重要的情況下,比如審核記帳數(shù)據(jù)庫(kù)以確保一個(gè)帳戶的所有借方金額和貸方金額的總數(shù)等于它目前的余額時(shí),可能還需要防止創(chuàng)建新行。這種情況下就需要使用“可串行化的”隔離級(jí)別。

    最低的隔離級(jí)別“讀未提交的”很少使用。它適用于您只需要獲得近似值,否則查詢將導(dǎo)致您不希望的性能開銷這種情況。當(dāng)您想要估計(jì)一個(gè)變化很快的數(shù)量,如定單數(shù)或者今天所下定單的總金額(以美元為單位)時(shí)一般使用““讀未提交的”。

    因?yàn)楦綦x和可伸縮性之間實(shí)際是一種此消彼長(zhǎng)的關(guān)系,所以您在為事務(wù)選擇隔離級(jí)別時(shí)應(yīng)該小心行事。選擇太低的級(jí)別對(duì)數(shù)據(jù)比較危險(xiǎn)。選擇太高的級(jí)別可能對(duì)性能不利,盡管負(fù)載比較輕時(shí)可能不會(huì)這樣。一般來說,數(shù)據(jù)一致性問題比性能問題更嚴(yán)重。如果拿不準(zhǔn),應(yīng)該以小心為主,選擇一個(gè)較高的隔離級(jí)別。這就引出了規(guī)則 5:

    規(guī)則 5:使用保證數(shù)據(jù)安全的最低隔離級(jí)別,但如果拿不準(zhǔn),請(qǐng)使用“可串行化的”。

    即使您打算剛開始時(shí)以小心為主并希望結(jié)果性能可以接受 ―(被稱為“拒絕和祈禱(denial and prayer)”的性能管理技術(shù) ― 很可能是最常用的性能策略,盡管大多數(shù)開發(fā)者都不承認(rèn)這一點(diǎn)),在開發(fā)組件時(shí)考慮隔離需求也是有利的。您應(yīng)該努力編寫能夠容忍級(jí)別較低但實(shí)用的隔離級(jí)別的事務(wù),這樣,當(dāng)稍后性能成為問題時(shí),自己就不會(huì)陷入困境。因?yàn)槟枰婪椒ㄕ谧鍪裁匆约斑@個(gè)方法中隱藏了什么一致性假設(shè)來正確設(shè)置隔離級(jí)別,那么在開發(fā)期間仔細(xì)說明并發(fā)需求和假設(shè),以便在裝配應(yīng)用程序時(shí)幫助作出正確的決定也不失為一個(gè)好主意。





    回頁(yè)首


    結(jié)束語(yǔ)

    本文中提供的許多指導(dǎo)可能看起來有點(diǎn)互相矛盾,因?yàn)橄笫聞?wù)劃分和隔離這種問題本來就是此消彼長(zhǎng)的。我們正在努力平衡安全性(如果我們不關(guān)心安全性,那就壓根不必用事務(wù)了)和我們用來提供安全限度的工具的性能開銷。正確的平衡要依賴許多因素,包括與系統(tǒng)故障或當(dāng)機(jī)時(shí)間相關(guān)的代價(jià)或損害以及組織的風(fēng)險(xiǎn)承受能力。

    posted @ 2006-08-24 17:38 Binary 閱讀(206) | 評(píng)論 (0)編輯 收藏

    Java 理論與實(shí)踐: 在沒有數(shù)據(jù)庫(kù)的情況下進(jìn)行數(shù)據(jù)庫(kù)查詢

    手里有錘子的時(shí)候,看什么東西都像釘子(就像古諺語(yǔ)所說的那樣)。但是如果沒有錘子時(shí)該怎樣辦呢?有時(shí),您可以去借一把錘子。然后,拿著這把借來的錘子敲打虛擬的釘子,最后歸還錘子,沒人知道這些。在本月的 Java 理論與實(shí)踐 系列中,Brian Goetz 將演示如何將 SQL 或者 XQuery 這樣的數(shù)據(jù)操縱之錘應(yīng)用于非持久存儲(chǔ)的數(shù)據(jù)。請(qǐng)?jiān)诒疚母綆У?討論論壇 中與作者和其他讀者分享您對(duì)本文的看法。(也可以單擊本文頂部或底部的 討論 來訪問該論壇。)

    我最近仔細(xì)考察了一個(gè)項(xiàng)目,該項(xiàng)目涉及相當(dāng)多的 Web 快速搜索。當(dāng)爬蟲程序爬過不同的 Web 站點(diǎn)時(shí),它將建立一個(gè)數(shù)據(jù)庫(kù),該數(shù)據(jù)庫(kù)中包括它所爬過的站點(diǎn)和網(wǎng)頁(yè)、每一頁(yè)所包含的鏈接、每一頁(yè)的分析結(jié)果等數(shù)據(jù)。最終結(jié)果是一組報(bào)告,詳細(xì)說明經(jīng)過了哪些站點(diǎn)和頁(yè)面、哪些是一直鏈接的、哪些鏈接已經(jīng)斷開、哪些頁(yè)面有錯(cuò)誤、計(jì)算出的頁(yè)面規(guī)格,等等。開始的時(shí)候,沒人確切知道需要什么樣的報(bào)告,或者應(yīng)當(dāng)采用什么樣的格式 —— 只知道有一些內(nèi)容要報(bào)告。這表明報(bào)告開發(fā)階段會(huì)是一個(gè)反復(fù)的階段,要經(jīng)過多次反饋、修改,并且可能嘗試使用不同的結(jié)構(gòu)。惟一確定的報(bào)告要求是,報(bào)告應(yīng)當(dāng)以 XML 形式展示,也可能以 HTML 形式展示。因此,開發(fā)和修改報(bào)告的過程必須是輕量級(jí)的,因?yàn)閳?bào)告要求是“動(dòng)態(tài)發(fā)現(xiàn)”的,而不是預(yù)先指定的。

    不需要數(shù)據(jù)庫(kù)

    對(duì)這個(gè)問題的“最顯而易見的”解決方法是將所有東西都放入 SQL 數(shù)據(jù)庫(kù)中 —— 頁(yè)面、鏈接、度量標(biāo)準(zhǔn)、HTTP 結(jié)果代碼、計(jì)時(shí)結(jié)果和其他元數(shù)據(jù)。這個(gè)問題可以借助關(guān)系表示來很好地解決,特別是因?yàn)檫@種方法不需要存儲(chǔ)已訪問頁(yè)面的內(nèi)容,只需要存儲(chǔ)它們的結(jié)構(gòu)和元數(shù)據(jù)。

    到目前為止,這個(gè)項(xiàng)目看起來像是一個(gè)典型的數(shù)據(jù)庫(kù)應(yīng)用程序,并且它并不缺少可供選擇的持久性策略。但是,或許可以避免使用數(shù)據(jù)庫(kù)持久存儲(chǔ)數(shù)據(jù)的復(fù)雜性 —— 這個(gè)快速搜索工具(crawler)只訪問數(shù)萬個(gè)頁(yè)面。這個(gè)數(shù)字不是很大,因此可以將整個(gè)數(shù)據(jù)庫(kù)放在內(nèi)存中,當(dāng)需要持久存儲(chǔ)數(shù)據(jù)時(shí),可以通過序列化來實(shí)現(xiàn)它。(是的,加載和保存操作要花費(fèi)較長(zhǎng)的時(shí)間,但是這些操作并不經(jīng)常執(zhí)行。)懶惰反而帶來了一個(gè)好處 —— 不需要處理持久性極大地縮短了開發(fā)應(yīng)用程序的時(shí)間,因而顯著地減少了開發(fā)工作量。構(gòu)建和操縱內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)要比每次添加、提取或者分析數(shù)據(jù)時(shí)都使用數(shù)據(jù)庫(kù)容易得多。不管選擇了哪種持久存儲(chǔ)模型,都會(huì)限制任何觸及到數(shù)據(jù)的代碼的構(gòu)造。

    內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)是一種樹型結(jié)構(gòu),如清單 1 所示,它的根是快速搜索過的各個(gè)網(wǎng)站的主頁(yè),因此 Visitor 模式是搜索這些主頁(yè)或者從中提取數(shù)據(jù)的理想模式。(構(gòu)建一個(gè)防止陷入鏈接循環(huán) —— A 鏈接到 B、B 鏈接到 C、C 鏈接到 A —— 的基本 Visitor 類并不是很難。)


    清單 1. Web 爬行器的一個(gè)簡(jiǎn)化方案
    												
    																		
    public class Site {
        Page homepage;
        Collection<Page> pages;
        Collection<Link> links;
    }
    
    public class Page {
        String url;
        Site site;
        PageMetrics metrics;
    }
    
    public class Link {
        Page linkFrom;
        Page linkTo;
        String anchorText;
    }
    
    												
    										

    這個(gè)快速搜索工具的應(yīng)用程序中有十多個(gè) Visitor,它們所做的事情類似于選擇頁(yè)面做進(jìn)一步分析、選擇不帶鏈接的頁(yè)面、列出“被鏈接最多”的頁(yè)面,等等。因?yàn)樗羞@些操作都很簡(jiǎn)單,所以 Visitor 模式(如清單 2 所示)可以工作得很好,由于數(shù)據(jù)結(jié)構(gòu)可以放到內(nèi)存中,因此就算進(jìn)行徹底搜索,花費(fèi)也不是很大:


    清單 2. 用于 Web 快速搜索工具數(shù)據(jù)庫(kù)的 Visitor 模式
    												
    																		
    public interface Visitor {
        public void visitSite(Site site);
        public void visitLink(Link link);
    }
    
    												
    										

    噢,忘記報(bào)告了

    如果不運(yùn)行報(bào)告的話,Visitor 策略在訪問數(shù)據(jù)方面會(huì)做得非常好。使用數(shù)據(jù)庫(kù)進(jìn)行持久存儲(chǔ)的一個(gè)好處是:在生成報(bào)告時(shí),SQL 的能力就會(huì)大放光彩 —— 幾乎可以讓數(shù)據(jù)庫(kù)做任何事情。甚至用 SQL 生成報(bào)告原型也很容易 —— 運(yùn)行原型報(bào)告,如果結(jié)果不是所需要的結(jié)果,那么可以修改 SQL 查詢或者編寫新的查詢,然后再試一試。如果改變的只是 SQL 查詢的話,那么這個(gè)編輯-編譯-運(yùn)行周期可能很快。如果 SQL 不是存儲(chǔ)在程序中,那么您甚至可以跳過這個(gè)周期的編譯部分,這樣可以快速生成報(bào)告的原型。確定所需要的報(bào)告后,將它們構(gòu)建到應(yīng)用程序中就很容易了。

    因此,雖然對(duì)于添加新結(jié)果、尋找特定的結(jié)果和進(jìn)行特殊傳輸來說,內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)都表現(xiàn)得很不錯(cuò),但是對(duì)于報(bào)告來說,這些變成了不利條件。對(duì)于所有其自身結(jié)構(gòu)與數(shù)據(jù)庫(kù)結(jié)構(gòu)不同的報(bào)告,Visitor 都必須創(chuàng)建一個(gè)全新的數(shù)據(jù)結(jié)構(gòu),以包含報(bào)告數(shù)據(jù)。因此,每一種報(bào)告類型都需要有自己的、特定于報(bào)告的中間數(shù)據(jù)結(jié)構(gòu)來存放結(jié)果,還需要一個(gè)用來填充中間數(shù)據(jù)結(jié)構(gòu)的訪問者,以及用來將中間數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換成最終報(bào)告的后處理(post-processing)代碼。似乎需要做很多工作,尤其在大多數(shù)原型報(bào)告將被拋棄時(shí)。例如,假定您想要列出所有從其他網(wǎng)站鏈接到某個(gè)給定網(wǎng)站的頁(yè)面的報(bào)告、所有外部頁(yè)面的列表報(bào)告,以及站點(diǎn)上鏈接該頁(yè)面的那些頁(yè)面的列表,然后,根據(jù)鏈接的數(shù)量對(duì)報(bào)告進(jìn)行歸類,鏈接最多的頁(yè)面顯示在最前面。這個(gè)計(jì)劃基本上將數(shù)據(jù)結(jié)構(gòu)從里到外翻了個(gè)個(gè)兒。為了用 Visitor 實(shí)現(xiàn)這種數(shù)據(jù)轉(zhuǎn)換,需要獲得從某個(gè)給定網(wǎng)站可以到達(dá)的外部頁(yè)面鏈接的列表,并根據(jù)被鏈接的頁(yè)面對(duì)它們進(jìn)行分類,如清單 3 所示:


    清單 3. Visitor 列出被鏈接最多的頁(yè)面,以及鏈接到它們的頁(yè)面
    												
    																		
    public class InvertLinksVisitor {
        public Map<Page, Set<Page>> map = ...;
        
        public void visitLink(Link link) {
            if (link.linkFrom.site.equals(targetSite) 
                && !link.linkTo.site.equals(targetSite)) {
                if (!map.containsKey(link.linkTo))
                    map.put(link.linkTo, new HashSet<Page>());
                map.get(link.linkTo).add(link.linkFrom);
            }
        }
    }
    
    												
    										

    清單 3 中的 Visitor 生成一個(gè)映射,將每一個(gè)外部頁(yè)面與鏈接它的一組內(nèi)部頁(yè)面相關(guān)聯(lián)。為了準(zhǔn)備該報(bào)告,還必須根據(jù)關(guān)聯(lián)頁(yè)面的大小對(duì)這些條目進(jìn)行分類,然后創(chuàng)建報(bào)告。雖然沒有任何困難步驟,但是每一個(gè)報(bào)告需要的特定于報(bào)告的代碼數(shù)量卻很多,因此快速報(bào)告原型就成為一個(gè)重要的目標(biāo)(因?yàn)闆]有提出報(bào)告要求),試驗(yàn)新報(bào)告的開銷比理想情況更高。許多報(bào)告需要多次傳遞數(shù)據(jù),以便對(duì)數(shù)據(jù)進(jìn)行選擇、匯總和分類。





    回頁(yè)首


    我的數(shù)據(jù)模型王國(guó)

    這時(shí),缺少一個(gè)正式的數(shù)據(jù)模型開始成為一項(xiàng)不利因素,該數(shù)據(jù)模型可以用于描述收集的數(shù)據(jù),并且可以用它更容易地表示選擇和聚合查詢。也許懶惰不像開始希望的那樣有效。但是,雖然這個(gè)應(yīng)用程序缺少正式數(shù)據(jù)模型,但也許我們可以將數(shù)據(jù)存儲(chǔ)到內(nèi)存中的數(shù)據(jù)庫(kù),并憑借該數(shù)據(jù)庫(kù)進(jìn)行查詢,通過這種方式借用一個(gè)數(shù)據(jù)模型。有兩種可能會(huì)立即出現(xiàn)在您的腦海中:開源的內(nèi)存中的 SQL 數(shù)據(jù)庫(kù) HSQLDB 和 XQuery。我不需要數(shù)據(jù)庫(kù)提供的持久性,但是我確實(shí)需要查詢語(yǔ)言。

    HSQLDB 是一個(gè)用 Java 語(yǔ)言編寫的可嵌入的數(shù)據(jù)庫(kù)引擎。它既包含適用于內(nèi)存中表的表類型,又包含適用于基于磁盤的表的表類型,設(shè)計(jì)該引擎為了將表完全嵌入到應(yīng)用程序中,消除與大多數(shù)真實(shí)數(shù)據(jù)庫(kù)相關(guān)的管理開銷。要將數(shù)據(jù)裝載到 HSQLDB,只需編寫一個(gè) Visitor 即可,該 Visitor 將遍歷內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),并為每一個(gè)將要存儲(chǔ)的實(shí)體生成相應(yīng)的 INSERT 語(yǔ)句。然后可以對(duì)這個(gè)內(nèi)存中的數(shù)據(jù)庫(kù)表執(zhí)行 SQL 查詢,以生成報(bào)告,并在完成這些操作后拋棄這個(gè)“數(shù)據(jù)庫(kù)”。

    噢,忘記了關(guān)系數(shù)據(jù)庫(kù)有多煩人

    HSQLDB 方法是一個(gè)可行方法,但您很快就發(fā)現(xiàn),我必須為對(duì)象關(guān)系的不匹配而兩次(而不是一次)受罰 —— 一次是在將樹型結(jié)構(gòu)數(shù)據(jù)庫(kù)轉(zhuǎn)換為關(guān)系數(shù)據(jù)模型時(shí),一次是在將平面關(guān)系查詢結(jié)果轉(zhuǎn)換成結(jié)構(gòu)化的 XML 或者 HTML 結(jié)果集時(shí)。此外,將 JDBC ResultSet 后處理為 DOM 表示形式的 XML 或者 HTML 文檔也不是一項(xiàng)很容易的任務(wù),需要為每一個(gè)報(bào)告提供一些定制的編碼。因此雖然內(nèi)存中的 SQL 數(shù)據(jù)庫(kù) 的確 可以簡(jiǎn)化查詢,但是從數(shù)據(jù)庫(kù)中存入和取出數(shù)據(jù)所需要的額外代碼會(huì)抵消所有節(jié)省的代碼。





    回頁(yè)首


    讓 XQuery 來拯救您

    另一個(gè)容易得到的數(shù)據(jù)查詢方法是 XQuery。XQuery 的優(yōu)點(diǎn)是,它是為生成 XML 或者 HTML 文檔作為查詢結(jié)果而設(shè)計(jì)的,因此不需要對(duì)查詢結(jié)果進(jìn)行后處理。這種想法很有吸引力 —— 每個(gè)報(bào)告只有一層編碼,而不是兩層或者更多層。因此第一項(xiàng)任務(wù)是構(gòu)建一個(gè)表示整個(gè)數(shù)據(jù)集的 XML 文檔。設(shè)計(jì)一個(gè)簡(jiǎn)單的 XML 數(shù)據(jù)模型和編寫遍歷數(shù)據(jù)結(jié)構(gòu),并將每一個(gè)元素附加到一個(gè) DOM 文檔中的 Visitor 很簡(jiǎn)單。(不需要寫出這個(gè)文檔。可以將它保持在內(nèi)存中,用于查詢,然后在完成查詢時(shí)丟棄它。當(dāng)?shù)讓訑?shù)據(jù)改變時(shí),可以重新生成它。)之后,所有要做的就是編寫 XQuery 查詢,該查詢將選擇并聚集用于報(bào)告的數(shù)據(jù),并按最終需要的格式(XML 或 HTML)對(duì)它們進(jìn)行格式化。查詢可以存儲(chǔ)在單獨(dú)的文件中,以便進(jìn)行快速原型制造,因此,可支持多種報(bào)告格式。使用 Saxon 評(píng)估查詢的代碼如清單 4 中所示:


    清單 4. 執(zhí)行 XQuery 查詢并將結(jié)果序列化為 XML 或 HTML 文檔的代碼
    												
    																		
      String query = readFile(queryFile + ".xq");
      Configuration c = new Configuration();
      StaticQueryContext qp = new StaticQueryContext(c);
      XQueryExpression xe = qp.compileQuery(query);
      DynamicQueryContext dqc = new DynamicQueryContext(c);
      dqc.setContextNode(new DocumentWrapper(document, z.getName(), c));
      List result = xe.evaluate(dqc);
    
      FileOutputStream os = new FileOutputStream(fileName);
      XMLSerializer serializer = new XMLSerializer (os, format);
      serializer.asDOMSerializer();
    
      for(Iterator i = result.iterator(); i.hasNext(); ) {
          Object o = i.next();
          if (o instanceof Element)
              serializer.serialize((Element) o);
          else if (o instanceof Attr) {
              Element e = document.createElement("scalar");
              e.setTextContent(((Attr) o).getNodeValue());
              serializer.serialize(e);
          }
          else {
              Element e = document.createElement("scalar");
              e.setTextContent(o.toString());
              serializer.serialize(e);
          }
      }
      os.close(); 
    
    												
    										

    表示數(shù)據(jù)庫(kù)的 XML 文檔的結(jié)構(gòu)與內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)稍有不同,每一個(gè) <site> 元素都有嵌套的 <page> 元素,每一個(gè) <page> 元素都有嵌套的 <link> 元素,而每一個(gè) <link> 元素都有 <link-to> 和 <link-from> 元素。實(shí)踐證明,這種表示方法對(duì)于大多數(shù)報(bào)告都很方便。

    清單 5 顯示了一個(gè)示例 XQuery 報(bào)告,這個(gè)報(bào)告處理鏈接的選擇、分類和表示。它有幾個(gè)地方優(yōu)于 Visitor 方法 —— 不僅代碼少(因?yàn)椴樵冋Z(yǔ)言支持選擇、聚積和分類),而且所有報(bào)告的代碼 —— 選擇、聚積、分類和表示 —— 都在一個(gè)位置上。


    清單 5.生成鏈接次數(shù)最多的頁(yè)面的完整報(bào)告的 XQuery 代碼
    												
    																		
    <html>
    <head><title>被鏈接最多的頁(yè)面</title></head>
    <body>
    <ul>
    {
      let $links := //link[link-to/@siteUrl ne $targetSite
                           and link-from/@siteUrl eq $targetSite]
      for $page in distinct-values($links/link-to/@url)
      let $linkingPages := $links[link-to/@url eq $page]/link-from/@url
      order by count($linkingPages)
      return 
        <li>Page {$page}, {count($linkingPages)} links 
        <ul> {
          for $p in $linkingPages return <li>Linked from {$p/@url}</li>
        }
        </ul></li>
    }
    </ul> </body> </html>
    
    												
    										





    回頁(yè)首


    結(jié)束語(yǔ)

    從開發(fā)成本角度看,XQuery 方法已證實(shí)可以節(jié)約大量成本。樹型結(jié)構(gòu)對(duì)于構(gòu)建和搜索數(shù)據(jù)很理想,但對(duì)于報(bào)告,就不是很理想了。XML 方法很適合于報(bào)告(因?yàn)榭梢岳?XQuery 的能力),但是對(duì)于整個(gè)應(yīng)用程序的實(shí)現(xiàn),該方法還有很多不便,并會(huì)降低性能。因?yàn)閿?shù)據(jù)集的大小是可管理的 —— 只有幾十兆字節(jié),所以可以將數(shù)據(jù)從一種格式轉(zhuǎn)換為從開發(fā)的角度看最方便的另一種格式。更大的數(shù)據(jù)集,比如不能完全存儲(chǔ)到內(nèi)存中的數(shù)據(jù)集,會(huì)要求整個(gè)應(yīng)用程序都圍繞著一個(gè)數(shù)據(jù)庫(kù)構(gòu)建。雖然有許多處理數(shù)據(jù)持久性的好工具,但是它們需要的工作都比簡(jiǎn)單操縱內(nèi)存中數(shù)據(jù)結(jié)構(gòu)要多得多。如果數(shù)據(jù)集的大小合適,那么就可以同時(shí)利用這兩種方法的長(zhǎng)處。

    posted @ 2006-08-24 17:36 Binary 閱讀(319) | 評(píng)論 (0)編輯 收藏

    Java 理論與實(shí)踐: 動(dòng)態(tài)編譯與性能測(cè)量

    為動(dòng)態(tài)編譯的語(yǔ)言(例如 Java)編寫和解釋性能評(píng)測(cè),要比為靜態(tài)編譯的語(yǔ)言(例如 C 或 C++)編寫困難得多。在這期的 Java 理論與實(shí)踐 中,Brian Goetz 介紹了動(dòng)態(tài)編譯使性能測(cè)試復(fù)雜的諸多原因中的一些。請(qǐng)?jiān)诒疚母綆У挠懻摻M上與作者和其他讀者分享您對(duì)本文的看法。 (您也可以選擇本文頂部或底部的 討論 訪問論壇。)

    這個(gè)月,我著手撰寫一篇文章,分析一個(gè)寫得很糟糕的微評(píng)測(cè)。畢竟,我們的程序員一直受性能困擾,我們也都想了解我們編寫、使用或批評(píng)的代碼的性能特征。當(dāng)我偶然間寫到性能這個(gè)主題時(shí),我經(jīng)常得到這樣的電子郵件:“我寫的這個(gè)程序顯示,動(dòng)態(tài) frosternation 要比靜態(tài) blestification 快,與您上一篇的觀點(diǎn)相反!”許多隨這類電子郵件而來的所謂“評(píng)測(cè)“程序,或者它們運(yùn)行的方式,明顯表現(xiàn)出他們對(duì)于 JVM 執(zhí)行字節(jié)碼的實(shí)際方式缺乏基本認(rèn)識(shí)。所以,在我著手撰寫這樣一篇文章(將在未來的專欄中發(fā)表)之前,我們先來看看 JVM 幕后的東西。理解動(dòng)態(tài)編譯和優(yōu)化,是理解如何區(qū)分微評(píng)測(cè)好壞的關(guān)鍵(不幸的是,好的微評(píng)測(cè)很少)。

    動(dòng)態(tài)編譯簡(jiǎn)史

    Java 應(yīng)用程序的編譯過程與靜態(tài)編譯語(yǔ)言(例如 C 或 C++)不同。靜態(tài)編譯器直接把源代碼轉(zhuǎn)換成可以直接在目標(biāo)平臺(tái)上執(zhí)行的機(jī)器代碼,不同的硬件平臺(tái)要求不同的編譯器。 Java 編譯器把 Java 源代碼轉(zhuǎn)換成可移植的 JVM 字節(jié)碼,所謂字節(jié)碼指的是 JVM 的“虛擬機(jī)器指令”。與靜態(tài)編譯器不同,javac 幾乎不做什么優(yōu)化 —— 在靜態(tài)編譯語(yǔ)言中應(yīng)當(dāng)由編譯器進(jìn)行的優(yōu)化工作,在 Java 中是在程序執(zhí)行的時(shí)候,由運(yùn)行時(shí)執(zhí)行。

    第一代 JVM 完全是解釋的。JVM 解釋字節(jié)碼,而不是把字節(jié)碼編譯成機(jī)器碼并直接執(zhí)行機(jī)器碼。當(dāng)然,這種技術(shù)不會(huì)提供最好的性能,因?yàn)橄到y(tǒng)在執(zhí)行解釋器上花費(fèi)的時(shí)間,比在需要運(yùn)行的程序上花費(fèi)的時(shí)間還要多。

    即時(shí)編譯

    對(duì)于證實(shí)概念的實(shí)現(xiàn)來說,解釋是合適的,但是早期的 JVM 由于太慢,迅速獲得了一個(gè)壞名聲。下一代 JVM 使用即時(shí) (JIT) 編譯器來提高執(zhí)行速度。按照嚴(yán)格的定義,基于 JIT 的虛擬機(jī)在執(zhí)行之前,把所有字節(jié)碼轉(zhuǎn)換成機(jī)器碼,但是以惰性方式來做這項(xiàng)工作:JIT 只有在確定某個(gè)代碼路徑將要執(zhí)行的時(shí)候,才編譯這個(gè)代碼路徑(因此有了名稱“ 即時(shí) 編譯”)。這個(gè)技術(shù)使程序能啟動(dòng)得更快,因?yàn)樵陂_始執(zhí)行之前,不需要冗長(zhǎng)的編譯階段。

    JIT 技術(shù)看起來很有前途,但是它有一些不足。JIT 消除了解釋的負(fù)擔(dān)(以額外的啟動(dòng)成本為代價(jià)),但是由于若干原因,代碼的優(yōu)化等級(jí)仍然是一般般。為了避免 Java 應(yīng)用程序嚴(yán)重的啟動(dòng)延遲,JIT 編譯器必須非常迅速,這意味著它無法把大量時(shí)間花在優(yōu)化上。所以,早期的 JIT 編譯器在進(jìn)行內(nèi)聯(lián)假設(shè)(inlining assumption)方面比較保守,因?yàn)樗鼈儾恢篮竺婵赡芤b入哪個(gè)類。

    雖然從技術(shù)上講,基于 JIT 的虛擬機(jī)在執(zhí)行字節(jié)碼之前,要先編譯字節(jié)碼,但是 JIT 這個(gè)術(shù)語(yǔ)通常被用來表示任何把字節(jié)碼轉(zhuǎn)換成機(jī)器碼的動(dòng)態(tài)編譯過程 —— 即使那些能夠解釋字節(jié)碼的過程也算。

    HotSpot 動(dòng)態(tài)編譯

    HotSpot 執(zhí)行過程組合了編譯、性能分析以及動(dòng)態(tài)編譯。它沒有把所有要執(zhí)行的字節(jié)碼轉(zhuǎn)換成機(jī)器碼,而是先以解釋器的方式運(yùn)行,只編譯“熱門”代碼 —— 執(zhí)行得最頻繁的代碼。當(dāng) HotSpot 執(zhí)行時(shí),會(huì)搜集性能分析數(shù)據(jù),用來決定哪個(gè)代碼段執(zhí)行得足夠頻繁,值得編譯。只編譯執(zhí)行最頻繁的代碼有幾項(xiàng)性能優(yōu)勢(shì):沒有把時(shí)間浪費(fèi)在編譯那些不經(jīng)常執(zhí)行的代碼上;這樣,編譯器就可以花更多時(shí)間來優(yōu)化熱門代碼路徑,因?yàn)樗涝谶@上面花的時(shí)間物有所值。而且,通過延遲編譯,編譯器可以訪問性能分析數(shù)據(jù),并用這些數(shù)據(jù)來改進(jìn)優(yōu)化決策,例如是否需要內(nèi)聯(lián)某個(gè)方法調(diào)用。

    為了讓事情變得更復(fù)雜,HotSpot 提供了兩個(gè)編譯器:客戶機(jī)編譯器和服務(wù)器編譯器。默認(rèn)采用客戶機(jī)編譯器;在啟動(dòng) JVM 時(shí),您可以指定 -server 開關(guān),選擇服務(wù)器編譯器。服務(wù)器編譯器針對(duì)最大峰值操作速度進(jìn)行了優(yōu)化,適用于需要長(zhǎng)期運(yùn)行的服務(wù)器應(yīng)用程序。客戶機(jī)編譯器的優(yōu)化目標(biāo),是減少應(yīng)用程序的啟動(dòng)時(shí)間和內(nèi)存消耗,優(yōu)化的復(fù)雜程度遠(yuǎn)遠(yuǎn)低于服務(wù)器編譯器,因此需要的編譯時(shí)間也更少。

    HotSpot 服務(wù)器編譯器能夠執(zhí)行各種樣的類。它能夠執(zhí)行許多靜態(tài)編譯器中常見的標(biāo)準(zhǔn)優(yōu)化,例如代碼提升( hoisting)、公共的子表達(dá)式清除、循環(huán)展開(unrolling)、范圍檢測(cè)清除、死代碼清除、數(shù)據(jù)流分析,還有各種在靜態(tài)編譯語(yǔ)言中不實(shí)用的優(yōu)化技術(shù),例如虛方法調(diào)用的聚合內(nèi)聯(lián)。

    持續(xù)重新編譯

    HotSpot 技術(shù)另一個(gè)有趣的方面是:編譯不是一個(gè)全有或者全無(all-or-nothing)的命題。在解釋代碼路徑一定次數(shù)之后,會(huì)把它重新編譯成機(jī)器碼。但是 JVM 會(huì)繼續(xù)進(jìn)行性能分析,而且如果認(rèn)為代碼路徑特別熱門,或者未來的性能分析數(shù)據(jù)認(rèn)為存在額外的優(yōu)化可能,那么還有可能用更高一級(jí)的優(yōu)化重新編譯代碼。JVM 在一個(gè)應(yīng)用程序的執(zhí)行過程中,可能會(huì)把相同的字節(jié)碼重新編譯許多次。為了深入了解編譯器做了什么,請(qǐng)用 -XX:+PrintCompilation 標(biāo)志調(diào)用 JVM,這個(gè)標(biāo)志會(huì)使編譯器(客戶機(jī)或服務(wù)器)每次運(yùn)行的時(shí)候打印一條短消息。

    棧上(On-stack)替換

    HotSpot 開始的版本編譯的時(shí)候每次編譯一個(gè)方法。如果某個(gè)方法的累計(jì)執(zhí)行次數(shù)超過指定的循環(huán)迭代次數(shù)(在 HotSpot 的第一版中,是 10,000 次),那么這個(gè)方法就被當(dāng)作熱門方法,計(jì)算的方式是:為每個(gè)方法關(guān)聯(lián)一個(gè)計(jì)數(shù)器,每次執(zhí)行一個(gè)后向分支時(shí),就會(huì)遞增計(jì)數(shù)器一次。但是,在方法編譯之后,方法調(diào)用并沒有切換到編譯的版本,需要退出并重新進(jìn)入方法,后續(xù)調(diào)用才會(huì)使用編譯的版本。結(jié)果就是,在某些情況下,可能永遠(yuǎn)不會(huì)用到編譯的版本,例如對(duì)于計(jì)算密集型程序,在這類程序中所有的計(jì)算都是在方法的一次調(diào)用中完成的。重量級(jí)方法可能被編譯,但是編譯的代碼永遠(yuǎn)用不到。

    HotSpot 最近的版本采用了稱為 棧上(on-stack)替換 (OSR) 的技術(shù),支持在循環(huán)過程中間,從解釋執(zhí)行切換到編譯的代碼(或者從編譯代碼的一個(gè)版本切換到另一個(gè)版本)。





    回頁(yè)首


    那么,這與評(píng)測(cè)有什么關(guān)系?

    我向您許諾了一篇關(guān)于評(píng)測(cè)和性能測(cè)量的文章,但是迄今為止,您得到的只是歷史的教訓(xùn)和 Sun 的 HotSpot 白皮書的老調(diào)重談。繞這么大的圈子的原因是,如果不理解動(dòng)態(tài)編譯的過程,就不可能正確地編寫或解釋 Java 類的性能測(cè)試。(即使深入理解動(dòng)態(tài)編譯和 JVM 優(yōu)化,也仍然是非常困難的。)

    為 Java 代碼編寫微評(píng)測(cè)遠(yuǎn)比為 C 代碼編寫難得多

    判斷方法 A 是否比方法 B 更快的傳統(tǒng)方法,是編寫小的評(píng)測(cè)程序,通常叫做 微評(píng)測(cè)。這個(gè)趨勢(shì)非常有意義。科學(xué)的方法不能缺少獨(dú)立的調(diào)查。魔鬼總在細(xì)節(jié)之中。為動(dòng)態(tài)編譯的語(yǔ)言編寫并解釋評(píng)測(cè),遠(yuǎn)比為靜態(tài)編譯的語(yǔ)言難得多。為了了解某個(gè)結(jié)構(gòu)的性能,編寫一個(gè)使用該結(jié)構(gòu)的程序一點(diǎn)也沒有錯(cuò),但是在許多情況下,用 Java 編寫的微評(píng)測(cè)告訴您的,往往與您所認(rèn)為的不一樣。

    使用 C 程序時(shí),您甚至不用運(yùn)行它,就能了解許多程序可能的性能特征。只要看看編譯出的機(jī)器碼就可以了。編譯器生成的指令就是將要執(zhí)行的機(jī)器碼,一般情況下,可以很合理地理解它們的時(shí)間特征。(有許多有毛病的例子,因?yàn)榭偸沁z漏分支預(yù)測(cè)或緩存,所以性能差的程度遠(yuǎn)遠(yuǎn)超過查看機(jī)器碼所能夠想像的程度,但是大多數(shù)情況下,您都可以通過查看機(jī)器碼了解 C 程序的性能的很多方面。)

    如果編譯器認(rèn)為某段代碼不恰當(dāng),準(zhǔn)備把它優(yōu)化掉(通常的情況是,評(píng)測(cè)到它實(shí)際上不做任何事情),那么您在生成的機(jī)器碼中可以看到這個(gè)優(yōu)化 —— 代碼不在那兒了。通常,對(duì)于 C 代碼,您不必執(zhí)行很長(zhǎng)時(shí)間,就可以對(duì)它的性能做出合理的推斷。

    而在另一方面,HotSpot JIT 在程序運(yùn)行時(shí)會(huì)持續(xù)地把 Java 字節(jié)碼重新編譯成機(jī)器碼,而重新編譯觸發(fā)的次數(shù)無法預(yù)期,觸發(fā)重新編譯的依據(jù)是性能分析數(shù)據(jù)積累到一定數(shù)量、裝入新類,或者執(zhí)行到的代碼路徑的類已經(jīng)裝入,但是還沒有執(zhí)行過。持續(xù)的重新編譯情況下的時(shí)間測(cè)量會(huì)非常混亂、讓人誤解,而且要想獲得有用的性能數(shù)據(jù),通常必須讓 Java 代碼運(yùn)行相當(dāng)長(zhǎng)的時(shí)間(我曾經(jīng)看到過一些怪事,在程序啟動(dòng)運(yùn)行之后要加速幾個(gè)小時(shí)甚至數(shù)天),才能獲得有用的性能數(shù)據(jù)。





    回頁(yè)首


    清除死代碼

    編寫好評(píng)測(cè)的一個(gè)挑戰(zhàn)就是,優(yōu)化編譯器要擅長(zhǎng)找出死代碼 —— 對(duì)于程序執(zhí)行的輸出沒有作用的代碼。但是評(píng)測(cè)程序一般不產(chǎn)生任何輸出,這就意味著有一些,或者全部代碼都有可能被優(yōu)化掉,而毫無知覺,這時(shí)您實(shí)際測(cè)量的執(zhí)行要少于您設(shè)想的數(shù)量。具體來說,許多微評(píng)測(cè)在用 -server 方式運(yùn)行時(shí),要比用 -client 方式運(yùn)行時(shí)好得多,這不是因?yàn)榉?wù)器編譯器更快(雖然服務(wù)器編譯器一般更快),而是因?yàn)榉?wù)器編譯器更擅長(zhǎng)優(yōu)化掉死代碼。不幸的是,能夠讓您的評(píng)測(cè)工作非常短(可能會(huì)把評(píng)測(cè)完全優(yōu)化掉)的死代碼優(yōu)化,在處理實(shí)際做些工作的代碼時(shí),做得就不會(huì)那么好了。

    奇怪的結(jié)果

    清單 1 的評(píng)測(cè)包含一個(gè)什么也不做的代碼塊,它是從一個(gè)測(cè)試并發(fā)線程性能的評(píng)測(cè)中摘出來的,但是它實(shí)際測(cè)量的根本不是要評(píng)測(cè)的東西。(這個(gè)示例是從 JavaOne 2003 的演示 “The Black Art of Benchmarking” 中借用的。請(qǐng)參閱 參考資料。)


    清單 1. 被意料之外的死代碼弄亂的評(píng)測(cè)
    												
    																		
            
    public class StupidThreadTest {
        public static void doSomeStuff() {
            double uselessSum = 0;
            for (int i=0; i<1000; i++) {
                for (int j=0;j<1000; j++) {
                    uselessSum += (double) i + (double) j;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            doSomeStuff();
            
            int nThreads = Integer.parseInt(args[0]);
            Thread[] threads = new Thread[nThreads];
            for (int i=0; i<nThreads; i++)
                threads[i] = new Thread(new Runnable() {
                    public void run() { doSomeStuff(); }
                });
            long start = System.currentTimeMillis();
            for (int i = 0; i < threads.length; i++)
                threads[i].start();
            for (int i = 0; i < threads.length; i++)
                threads[i].join();
            long end = System.currentTimeMillis();
            System.out.println("Time: " + (end-start) + "ms");
        }
    }
    
          
    												
    										

    表面上看, doSomeStuff() 方法可以給線程分點(diǎn)事做,所以我們能夠從 StupidThreadBenchmark 的運(yùn)行時(shí)間推導(dǎo)出多線程調(diào)度開支的一些情況。但是,因?yàn)?uselessSum 從沒被用過,所以編譯器能夠判斷出 doSomeStuff 中的全部代碼是死的,然后把它們?nèi)績(jī)?yōu)化掉。一旦循環(huán)中的代碼消失,循環(huán)也就消失了,只留下一個(gè)空空如也的 doSomeStuff。表 1 顯示了使用客戶機(jī)和服務(wù)器方式執(zhí)行 StupidThreadBenchmark 的性能。兩個(gè) JVM 運(yùn)行大量線程的時(shí)候,都表現(xiàn)出差不多是線性的運(yùn)行時(shí)間,這個(gè)結(jié)果很容易被誤解為服務(wù)器 JVM 比客戶機(jī) JVM 快 40 倍。而實(shí)際上,是服務(wù)器編譯器做了更多優(yōu)化,發(fā)現(xiàn)整個(gè) doSomeStuff 是死代碼。雖然確實(shí)有許多程序在服務(wù)器 JVM 上會(huì)提速,但是您在這里看到的提速僅僅代表一個(gè)寫得糟糕的評(píng)測(cè),而不能成為服務(wù)器 JVM 性能的證明。但是如果您沒有細(xì)看,就很容易會(huì)把兩者混淆。


    表 1. 在客戶機(jī)和服務(wù)器 JVM 中 StupidThreadBenchmark 的性能
    線程數(shù)量 客戶機(jī) JVM 運(yùn)行時(shí)間 服務(wù)器 JVM 運(yùn)行時(shí)間
    10 43 2
    100 435 10
    1000 4142 80
    10000 42402 1060

    對(duì)于評(píng)測(cè)靜態(tài)編譯語(yǔ)言來說,處理過于積極的死代碼清除也是一個(gè)問題。但是,在靜態(tài)編譯語(yǔ)言中,能夠更容易地發(fā)現(xiàn)編譯器清除了大塊評(píng)測(cè)。您可以查看生成的機(jī)器碼,查看是否漏了某塊程序。而對(duì)于動(dòng)態(tài)編譯語(yǔ)言,這些信息不太容易訪問得到。





    回頁(yè)首


    預(yù)熱

    如果您想測(cè)量 X 的性能,一般情況下您是想測(cè)量它編譯后的性能,而不是它的解釋性能(您想知道 X 在賽場(chǎng)上能跑多快)。要做到這樣,需要“預(yù)熱” JVM —— 即讓目標(biāo)操作執(zhí)行足夠的時(shí)間,這樣編譯器在為執(zhí)行計(jì)時(shí)之前,就有足夠的運(yùn)行解釋的代碼,并用編譯的代碼替換解釋代碼。

    使用早期 JIT 和沒有棧上替換的動(dòng)態(tài)編譯器,有一個(gè)容易的公式可以測(cè)量方法編譯后的性能:運(yùn)行多次調(diào)用,啟動(dòng)計(jì)時(shí)器,然后執(zhí)行若干次方法。如果預(yù)熱調(diào)用超過方法被編譯的閾值,那么實(shí)際計(jì)時(shí)的調(diào)用就有可能全部是編譯代碼執(zhí)行的時(shí)間,所有的編譯開支應(yīng)當(dāng)在開始計(jì)時(shí)之前發(fā)生。

    而使用今天的動(dòng)態(tài)編譯器,事情更困難。編譯器運(yùn)行的次數(shù)很難預(yù)測(cè),JVM 按照自己的想法從解釋代碼切換到編譯代碼,而且在運(yùn)行期間,相同的代碼路徑可能編譯、重新編譯不止一次。如果您不處理這些事件的計(jì)時(shí)問題,那么它們會(huì)嚴(yán)重歪曲您的計(jì)時(shí)結(jié)果。

    圖 1 顯示了由于預(yù)計(jì)不到的動(dòng)態(tài)編譯而造成的可能的計(jì)時(shí)歪曲。假設(shè)您正在通過循環(huán)計(jì)時(shí) 200,000 次迭代,編譯代碼比解釋代碼快 10 倍。如果編譯只在 200,000 次迭代時(shí)才發(fā)生,那么您測(cè)量的只是解釋代碼的性能(時(shí)間線(a))。如果編譯在 100,000 次迭代時(shí)發(fā)生,那么您總共的運(yùn)行時(shí)間是運(yùn)行 200,000 次解釋迭代的時(shí)間,加上編譯時(shí)間(編譯時(shí)間非您所愿),加上執(zhí)行 100,000 次編譯迭代的時(shí)間(時(shí)間線(b))。如果編譯在 20,000 次迭代時(shí)發(fā)生,那么總時(shí)間會(huì)是 20,000 次解釋迭代,加上編譯時(shí)間,再加上 180,000 次編譯迭代(時(shí)間線(c))。因?yàn)槟恢谰幾g器什么時(shí)候執(zhí)行,也不知道要執(zhí)行多長(zhǎng)時(shí)間,所以您可以看到,您的測(cè)量可能受到嚴(yán)重的歪曲。根據(jù)編譯時(shí)間和編譯代碼比解釋代碼快的程度,即使對(duì)迭代數(shù)量只做很小的變化,也可能造成測(cè)量的“性能”有極大差異。


    圖 1. 因?yàn)閯?dòng)態(tài)編譯計(jì)時(shí)造成的性能測(cè)量歪曲
    時(shí)間線圖

    那么,到底多少預(yù)熱才足夠呢?您不知道。您能做到的最好的,就是用 -XX:+PrintCompilation 開關(guān)來運(yùn)行評(píng)測(cè),觀察什么造成編譯器工作,然后改變?cè)u(píng)測(cè)程序的結(jié)構(gòu),以確保編譯在您啟動(dòng)計(jì)時(shí)之前發(fā)生,在計(jì)時(shí)循環(huán)過程中不會(huì)再發(fā)生編譯。

    不要忘記垃圾收集

    那么,您已經(jīng)看到,如果您想得到正確的計(jì)時(shí)結(jié)果,就必須要讓被測(cè)代碼比您想像的多運(yùn)行幾次,以便讓 JVM 預(yù)熱。另一方面,如果測(cè)試代碼要進(jìn)行對(duì)象分配工作(差不多所有的代碼都要這樣),那么垃圾收集器也肯定會(huì)運(yùn)行。這是會(huì)嚴(yán)重歪曲計(jì)時(shí)結(jié)果的另一個(gè)因素 —— 即使對(duì)迭代數(shù)量只做很小的變化,也意味著沒有垃圾收集和有垃圾收集之間的區(qū)別,就會(huì)偏離“每迭代時(shí)間”的測(cè)量。

    如果用 -verbose:gc 開關(guān)運(yùn)行評(píng)測(cè),您可以看到在垃圾收集上耗費(fèi)了多少時(shí)間,并相應(yīng)地調(diào)整您的計(jì)時(shí)數(shù)據(jù)。更好一些的話,您可以長(zhǎng)時(shí)間運(yùn)行您的程序,這可以保證觸發(fā)許多垃圾收集,從而更精確地分?jǐn)偫占某杀尽?





    回頁(yè)首


    動(dòng)態(tài)反優(yōu)化(deoptimization)

    許多標(biāo)準(zhǔn)的優(yōu)化只能在“基本塊”內(nèi)執(zhí)行,所以內(nèi)聯(lián)方法調(diào)用對(duì)于達(dá)到好的優(yōu)化通常很重要。通過內(nèi)聯(lián)方法調(diào)用,不僅方法調(diào)用的開支被清除,而且給優(yōu)化器提供了更大的優(yōu)化塊可以優(yōu)化,會(huì)帶來相當(dāng)大的死代碼優(yōu)化機(jī)會(huì)。

    清單 2 顯示了一個(gè)通過內(nèi)聯(lián)實(shí)現(xiàn)的這類優(yōu)化的示例。 outer() 方法用參數(shù) null 調(diào)用 inner(),結(jié)果是 inner() 什么也不做。但是通過把 inner() 的調(diào)用內(nèi)聯(lián),編譯器可以發(fā)現(xiàn) inner()else 分支是死的,因此能夠把測(cè)試和 else 分支優(yōu)化掉,在某種程度上,它甚至能把整個(gè)對(duì) inner() 的調(diào)用全優(yōu)化掉。如果 inner() 沒有被內(nèi)聯(lián),那么這個(gè)優(yōu)化是不可能發(fā)生的。


    清單 2. 內(nèi)聯(lián)如何帶來更好的死代碼優(yōu)化
    												
    																		
            
    public class Inline {
      public final void inner(String s) {
        if (s == null)
          return;
        else {
          // do something really complicated
        }
      }
    
      public void outer() {
        String s=null; 
        inner(s);
      }
    }
    
          
    												
    										

    但是不方便的是,虛方法對(duì)內(nèi)聯(lián)造成了障礙,而虛函數(shù)調(diào)用在 Java 中要比在 C++ 中普遍。假設(shè)編譯器正試圖優(yōu)化以下代碼中對(duì) doSomething() 的調(diào)用:

    												
    														  Foo foo = getFoo();
      foo.doSomething(); 
    
    												
    										

    從這個(gè)代碼片斷中,編譯器沒有必要分清要執(zhí)行哪個(gè)版本的 doSomething() —— 是在類 Foo 中實(shí)現(xiàn)的版本,還是在 Foo 的子類中實(shí)現(xiàn)的版本?只在少數(shù)情況下答案才明顯 —— 例如 Foofinal 的,或者 doSomething()Foo 中被定義為 final 方法 —— 但是在多數(shù)情況下,編譯器不得不猜測(cè)。對(duì)于每次只編譯一個(gè)類的靜態(tài)編譯器,我們很幸運(yùn)。但是動(dòng)態(tài)編譯器可以使用全局信息進(jìn)行更好的決策。假設(shè)有一個(gè)還沒有裝入的類,它擴(kuò)展了應(yīng)用程序中的 Foo。現(xiàn)在的情景更像是 doSomething()Foo 中的 final 方法 —— 編譯器可以把虛方法調(diào)用轉(zhuǎn)換成一個(gè)直接分配(已經(jīng)是個(gè)改進(jìn)了),而且,還可以內(nèi)聯(lián) doSomething()。(把虛方法調(diào)用轉(zhuǎn)換成直接方法調(diào)用,叫做 單形(monomorphic)調(diào)用變換。)

    請(qǐng)稍等 —— 類可以動(dòng)態(tài)裝入。如果編譯器進(jìn)行了這樣的優(yōu)化,然后裝入了一個(gè)擴(kuò)展了 Foo 的類,會(huì)發(fā)生什么?更糟的是,如果這是在工廠方法 getFoo() 內(nèi)進(jìn)行的會(huì)怎么樣? getFoo() 會(huì)返回新的 Foo 子類的實(shí)例?那么,生成的代碼不就無效了么?對(duì),是無效了。但是 JVM 能指出這個(gè)錯(cuò)誤,并根據(jù)目前無效的假設(shè),取消生成的代碼,并恢復(fù)解釋(或者重新編譯不正確的代碼路徑)。

    結(jié)果就是,編譯器要進(jìn)行主動(dòng)的內(nèi)聯(lián)決策,才能得到更高的性能,然后當(dāng)這些決策依據(jù)的假設(shè)不再有效時(shí),就會(huì)收回這些決策。實(shí)際上,這個(gè)優(yōu)化如此有效,以致于給那些不被覆蓋的方法添加 final 關(guān)鍵字(一種性能技巧,在以前的文章中建議過)對(duì)于提高實(shí)際性能沒有太大作用。

    奇怪的結(jié)果

    清單 3 中包含一個(gè)代碼模式,其中組合了不恰當(dāng)?shù)念A(yù)熱、單形調(diào)用變換以及反優(yōu)化,因此生成的結(jié)果毫無意義,而且容易被誤解:


    清單 3. 測(cè)試程序的結(jié)果被單形調(diào)用變換和后續(xù)的反優(yōu)化歪曲
    												
    																		
            
    public class StupidMathTest {
        public interface Operator {
            public double operate(double d);
        }
    
        public static class SimpleAdder implements Operator {
            public double operate(double d) {
                return d + 1.0;
            }
        }
    
        public static class DoubleAdder implements Operator {
            public double operate(double d) {
                return d + 0.5 + 0.5;
            }
        }
    
        public static class RoundaboutAdder implements Operator {
            public double operate(double d) {
                return d + 2.0 - 1.0;
            }
        }
    
        public static void runABunch(Operator op) {
            long start = System.currentTimeMillis();
            double d = 0.0;
            for (int i = 0; i < 5000000; i++)
                d = op.operate(d);
            long end = System.currentTimeMillis();
            System.out.println("Time: " + (end-start) + "   ignore:" + d);
        }
    
        public static void main(String[] args) {
            Operator ra = new RoundaboutAdder();
            runABunch(ra); // misguided warmup attempt
            runABunch(ra);
            Operator sa = new SimpleAdder();
            Operator da = new DoubleAdder();
            runABunch(sa);
            runABunch(da);
        }
    }
    
          
    												
    										

    StupidMathTest 首先試圖做些預(yù)熱(沒有成功),然后測(cè)量 SimpleAdderDoubleAdderRoundaboutAdder 的運(yùn)行時(shí)間,結(jié)果如表 2 所示。看起來好像先加 1,再加 2 ,然后再減 1 最快。加兩次 0.5 比加 1 還快。這有可能么?(答案是:不可能。)


    表 2. StupidMathTest 毫無意義且令人誤解的結(jié)果
    方法 運(yùn)行時(shí)間
    SimpleAdder 88ms
    DoubleAdder 76ms
    RoundaboutAdder 14ms

    這里發(fā)生什么呢?在預(yù)熱循環(huán)之后, RoundaboutAdderrunABunch() 確實(shí)已經(jīng)被編譯了,而且編譯器 OperatorRoundaboutAdder 上進(jìn)行了單形調(diào)用轉(zhuǎn)換,第一輪運(yùn)行得非常快。而在第二輪( SimpleAdder)中,編譯器不得不反優(yōu)化,又退回虛函數(shù)分配之中,所以第二輪的執(zhí)行表現(xiàn)得更慢,因?yàn)椴荒馨烟摵瘮?shù)調(diào)用優(yōu)化掉,把時(shí)間花在了重新編譯上。在第三輪( DoubleAdder)中,重新編譯比第二輪少,所以運(yùn)行得就更快。(在現(xiàn)實(shí)中,編譯器會(huì)在 RoundaboutAdderDoubleAdder 上進(jìn)行常數(shù)替換(constant folding),生成與 SimpleAdder 幾乎相同的代碼。所以如果在運(yùn)行時(shí)間上有差異,那么不是因?yàn)樗阈g(shù)代碼)。哪個(gè)代碼首先執(zhí)行,哪個(gè)代碼就會(huì)最快。

    那么,從這個(gè)“評(píng)測(cè)”中,我們能得出什么結(jié)論呢?實(shí)際上,除了評(píng)測(cè)動(dòng)態(tài)編譯語(yǔ)言要比您可能想到的要微妙得多之外,什么也沒得到。





    回頁(yè)首


    結(jié)束語(yǔ)

    這個(gè)示例中的結(jié)果錯(cuò)得如此明顯,所以很清楚,肯定發(fā)生了什么,但是更小的結(jié)果能夠很容易地歪曲您的性能測(cè)試程序的結(jié)果,卻不會(huì)觸發(fā)您的“這里肯定有什么東西有問題”的警惕。雖然本文列出的這些內(nèi)容是微評(píng)測(cè)歪曲的一般來源,但是還有許多其他來源。本文的中心思想是:您正在測(cè)量的,通常不是您以為您正在測(cè)量的。實(shí)際上,您通常所測(cè)量的,不是您以為您正在測(cè)量的。對(duì)于那些沒有包含什么實(shí)際的程序負(fù)荷,測(cè)試時(shí)間不夠長(zhǎng)的性能測(cè)試的結(jié)果,一定要非常當(dāng)心。

    posted @ 2006-08-24 17:36 Binary 閱讀(235) | 評(píng)論 (0)編輯 收藏

    Java 理論與實(shí)踐: 用動(dòng)態(tài)代理進(jìn)行修飾

    動(dòng)態(tài)代理工具java.lang.reflect 包的一部分,在 JDK 1.3 版本中添加到 JDK,它允許程序創(chuàng)建 代理對(duì)象,代理對(duì)象能實(shí)現(xiàn)一個(gè)或多個(gè)已知接口,并用反射代替內(nèi)置的虛方法分派,編程地分派對(duì)接口方法的調(diào)用。這個(gè)過程允許實(shí)現(xiàn)“截取”方法調(diào)用,重新路由它們或者動(dòng)態(tài)地添加功能。本期文章中,Brian Goetz 介紹了幾個(gè)用于動(dòng)態(tài)代理的應(yīng)用程序。請(qǐng)?jiān)诒疚陌殡S的 討論論壇 上與作者和其他讀者分享您對(duì)這篇文章的想法。(也可以單擊文章頂部或底部的 討論 訪問討論論壇。)

    動(dòng)態(tài)代理為實(shí)現(xiàn)許多常見設(shè)計(jì)模式(包括 Facade、Bridge、Interceptor、Decorator、Proxy(包括遠(yuǎn)程和虛擬代理)和 Adapter 模式)提供了替代的動(dòng)態(tài)機(jī)制。雖然這些模式不使用動(dòng)態(tài)代理,只用普通的類就能夠?qū)崿F(xiàn),但是在許多情況下,動(dòng)態(tài)代理方式更方便、更緊湊,可以清除許多手寫或生成的類。

    Proxy 模式

    Proxy 模式中要?jiǎng)?chuàng)建“stub”或“surrogate”對(duì)象,它們的目的是接受請(qǐng)求并把請(qǐng)求轉(zhuǎn)發(fā)到實(shí)際執(zhí)行工作的其他對(duì)象。遠(yuǎn)程方法調(diào)用(RMI)利用 Proxy 模式,使得在其他 JVM 中執(zhí)行的對(duì)象就像本地對(duì)象一樣;企業(yè) JavaBeans (EJB)利用 Proxy 模式添加遠(yuǎn)程調(diào)用、安全性和事務(wù)分界;而 JAX-RPC Web 服務(wù)則用 Proxy 模式讓遠(yuǎn)程服務(wù)表現(xiàn)得像本地對(duì)象一樣。在每一種情況中,潛在的遠(yuǎn)程對(duì)象的行為是由接口定義的,而接口本質(zhì)上接受多種實(shí)現(xiàn)。調(diào)用者(在大多數(shù)情況下)不能區(qū)分出它們只是持有一個(gè)對(duì) stub 而不是實(shí)際對(duì)象的引用,因?yàn)槎邔?shí)現(xiàn)了相同的接口;stub 的工作是查找實(shí)際的對(duì)象、封送參數(shù)、把參數(shù)發(fā)送給實(shí)際對(duì)象、解除封送返回值、把返回值返回給調(diào)用者。代理可以用來提供遠(yuǎn)程控制(就像在 RMI、EJB 和 JAX-RPC 中那樣),用安全性策略包裝對(duì)象(EJB)、為昂貴的對(duì)象(EJB 實(shí)體 Bean)提供惰性裝入,或者添加檢測(cè)工具(例如日志記錄)。

    在 5.0 以前的 JDK 中,RMI stub(以及它對(duì)等的 skeleton)是在編譯時(shí)由 RMI 編譯器(rmic)生成的類,RMI 編譯器是 JDK 工具集的一部分。對(duì)于每個(gè)遠(yuǎn)程接口,都會(huì)生成一個(gè) stub(代理)類,它代表遠(yuǎn)程對(duì)象,還生成一個(gè) skeleton 對(duì)象,它在遠(yuǎn)程 JVM 中做與 stub 相反的工作 —— 解除封送參數(shù)并調(diào)用實(shí)際的對(duì)象。類似地,用于 Web 服務(wù)的 JAX-RPC 工具也為遠(yuǎn)程 Web 服務(wù)生成代理類,從而使遠(yuǎn)程 Web 服務(wù)看起來就像本地對(duì)象一樣。

    不管 stub 類是以源代碼還是以字節(jié)碼生成的,代碼生成仍然會(huì)向編譯過程添加一些額外步驟,而且因?yàn)槊嗨频念惖姆簽E,會(huì)帶來意義模糊的可能性。另一方面,動(dòng)態(tài)代理機(jī)制支持在編譯時(shí)沒有生成 stub 類的情況下,在運(yùn)行時(shí)創(chuàng)建代理對(duì)象。在 JDK 5.0 及以后版本中,RMI 工具使用動(dòng)態(tài)代理代替了生成的 stub,結(jié)果 RMI 變得更容易使用。許多 J2EE 容器也使用動(dòng)態(tài)代理來實(shí)現(xiàn) EJB。EJB 技術(shù)嚴(yán)重地依靠使用攔截(interception)來實(shí)現(xiàn)安全性和事務(wù)分界;動(dòng)態(tài)代理為接口上調(diào)用的所有方法提供了集中的控制流程路徑。





    回頁(yè)首


    動(dòng)態(tài)代理機(jī)制

    動(dòng)態(tài)代理機(jī)制的核心是 InvocationHandler 接口,如清單 1 所示。調(diào)用句柄的工作是代表動(dòng)態(tài)代理實(shí)際執(zhí)行所請(qǐng)求的方法調(diào)用。傳遞給調(diào)用句柄一個(gè) Method 對(duì)象(從 java.lang.reflect 包),參數(shù)列表則傳遞給方法;在最簡(jiǎn)單的情況下,可能僅僅是調(diào)用反射性的方法 Method.invoke() 并返回結(jié)果。


    清單 1. InvocationHandler 接口
    												
    																		
    public interface InvocationHandler {
        Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable;
    }
    
    												
    										

    每個(gè)代理都有一個(gè)與之關(guān)聯(lián)的調(diào)用句柄,只要代理的方法被調(diào)用時(shí)就會(huì)調(diào)用該句柄。根據(jù)通用的設(shè)計(jì)原則:接口定義類型、類定義實(shí)現(xiàn),代理對(duì)象可以實(shí)現(xiàn)一個(gè)或多個(gè)接口,但是不能實(shí)現(xiàn)類。因?yàn)榇眍悰]有可以訪問的名稱,它們不能有構(gòu)造函數(shù),所以它們必須由工廠創(chuàng)建。清單 2 顯示了動(dòng)態(tài)代理的最簡(jiǎn)單的可能實(shí)現(xiàn),它實(shí)現(xiàn) Set 接口并把所有 Set 方法(以及所有 Object 方法)分派給封裝的 Set 實(shí)例。


    清單 2. 包裝 Set 的簡(jiǎn)單的動(dòng)態(tài)代理
    												
    																		
    public class SetProxyFactory {
    
        public static Set getSetProxy(final Set s) {
            return (Set) Proxy.newProxyInstance
              (s.getClass().getClassLoader(),
                    new Class[] { Set.class },
                    new InvocationHandler() {
                        public Object invoke(Object proxy, Method method, 
                          Object[] args) throws Throwable {
                            return method.invoke(s, args);
                        }
                    });
        }
    }
    
    												
    										

    SetProxyFactory 類包含一個(gè)靜態(tài)工廠方法 getSetProxy(),它返回一個(gè)實(shí)現(xiàn)了 Set 的動(dòng)態(tài)代理。代理對(duì)象實(shí)際實(shí)現(xiàn) Set —— 調(diào)用者無法區(qū)分(除非通過反射)返回的對(duì)象是動(dòng)態(tài)代理。SetProxyFactory 返回的代理只做一件事,把方法分派給傳遞給工廠方法的 Set 實(shí)例。雖然反射代碼通常比較難讀,但是這里的內(nèi)容很少,跟上控制流程并不難 —— 只要某個(gè)方法在 Set 代理上被調(diào)用,它就被分派給調(diào)用句柄,調(diào)用句柄只是反射地調(diào)用底層包裝的對(duì)象上的目標(biāo)方法。當(dāng)然,絕對(duì)什么都不做的代理可能有點(diǎn)傻,是不是呢?

    什么都不做的適配器

    對(duì)于像 SetProxyFactory 這樣什么都不做的包裝器來說,實(shí)際有個(gè)很好的應(yīng)用 —— 可以用它安全地把對(duì)象引用的范圍縮小到特定接口(或接口集)上,方式是,調(diào)用者不能提升引用的類型,使得可以更安全地把對(duì)象引用傳遞給不受信任的代碼(例如插件或回調(diào))。清單 3 包含一組類定義,實(shí)現(xiàn)了典型的回調(diào)場(chǎng)景。從中會(huì)看到動(dòng)態(tài)代理可以更方便地替代通常用手工(或用 IDE 提供的代碼生成向?qū)В?shí)現(xiàn)的 Adapter 模式。


    清單 3. 典型的回調(diào)場(chǎng)景
    												
    																		
    public interface ServiceCallback {
        public void doCallback();
    }
    
    public interface Service {
        public void serviceMethod(ServiceCallback callback);
    }
    
    public class ServiceConsumer implements ServiceCallback {
        private Service service;
    
        ...
        public void someMethod() {
            ...
            service.serviceMethod(this);
        }
    }
    
    												
    										

    ServiceConsumer 類實(shí)現(xiàn)了 ServiceCallback(這通常是支持回調(diào)的一個(gè)方便途徑)并把 this 引用傳遞給 serviceMethod() 作為回調(diào)引用。這種方法的問題是沒有機(jī)制可以阻止 Service 實(shí)現(xiàn)把 ServiceCallback 提升為 ServiceConsumer,并調(diào)用 ServiceConsumer 不希望 Service 調(diào)用的方法。有時(shí)對(duì)這個(gè)風(fēng)險(xiǎn)并不關(guān)心 —— 但有時(shí)卻關(guān)心。如果關(guān)心,那么可以把回調(diào)對(duì)象作為內(nèi)部類,或者編寫一個(gè)什么都不做的適配器類(請(qǐng)參閱清單 4 中的 ServiceCallbackAdapter)并用 ServiceCallbackAdapter 包裝 ServiceConsumerServiceCallbackAdapter 防止 ServiceServiceCallback 提升為 ServiceConsumer


    清單 4. 用于安全地把對(duì)象限制在一個(gè)接口上以便不被惡意代碼不能的適配器類
    												
    																		
    public class ServiceCallbackAdapter implements ServiceCallback {
        private final ServiceCallback cb;
    
        public ServiceCallbackAdapter(ServiceCallback cb) {
            this.cb = cb;
        }
    
        public void doCallback() {
            cb.doCallback();
        }
    }
    
    												
    										

    編寫 ServiceCallbackAdapter 這樣的適配器類簡(jiǎn)單卻乏味。必須為包裝的接口中的每個(gè)方法編寫重定向類。在 ServiceCallback 的示例中,只有一個(gè)需要實(shí)現(xiàn)的方法,但是某些接口,例如 Collections 或 JDBC 接口,則包含許多方法。現(xiàn)代的 IDE 提供了“Delegate Methods”向?qū)В档土司帉戇m配器類的工作量,但是仍然必須為每個(gè)想要包裝的接口編寫一個(gè)適配器類,而且對(duì)于只包含生成的代碼的類,也有一些讓人不滿意的地方。看起來應(yīng)當(dāng)有一種方式可以更緊湊地表示“什么也不做的限制適配器模式”。

    通用適配器類

    清單 2 中的 SetProxyFactory 類當(dāng)然比用于 Set 的等價(jià)的適配器類更緊湊,但是它仍然只適用于一個(gè)接口:Set。但是通過使用泛型,可以容易地創(chuàng)建通用的代理工廠,由它為任何接口做同樣的工作,如清單 5 所示。它幾乎與 SetProxyFactory 相同,但是可以適用于任何接口。現(xiàn)在再也不用編寫限制適配器類了!如果想創(chuàng)建代理對(duì)象安全地把對(duì)象限制在接口 T,只要調(diào)用 getProxy(T.class,object) 就可以了,不需要一堆適配器類的額外累贅。


    清單 5. 通用的限制適配器工廠類
    												
    																		
    public class GenericProxyFactory {
    
        public static<T> T getProxy(Class<T> intf, 
          final T obj) {
            return (T) 
              Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                    new Class[] { intf },
                    new InvocationHandler() {
                        public Object invoke(Object proxy, Method method, 
                          Object[] args) throws Throwable {
                            return method.invoke(obj, args);
                        }
                    });
        }
    }
    
    												
    										





    回頁(yè)首


    動(dòng)態(tài)代理作為 Decorator

    當(dāng)然,動(dòng)態(tài)代理工具能做的,遠(yuǎn)不僅僅是把對(duì)象類型限制在特定接口上。從 清單 2清單 5 中簡(jiǎn)單的限制適配器到 Decorator 模式,是一個(gè)小的飛躍,在 Decorator 模式中,代理用額外的功能(例如安全檢測(cè)或日志記錄)包裝調(diào)用。清單 6 顯示了一個(gè)日志 InvocationHandler,它在調(diào)用目標(biāo)對(duì)象上的方法之外,還寫入一條日志信息,顯示被調(diào)用的方法、傳遞的參數(shù),以及返回值。除了反射性的 invoke() 調(diào)用之外,這里的全部代碼只是生成調(diào)試信息的一部分 —— 還不是太多。代理工廠方法的代碼幾乎與 GenericProxyFactory 相同,區(qū)別在于它使用的是 LoggingInvocationHandler 而不是匿名的調(diào)用句柄。


    清單 6. 基于代理的 Decorator,為每個(gè)方法調(diào)用生成調(diào)試日志
    												
    																		
        private static class LoggingInvocationHandler<T> 
          implements InvocationHandler {
            final T underlying;
    
            public LoggingHandler(T underlying) {
                this.underlying = underlying;
            }
    
            public Object invoke(Object proxy, Method method, 
              Object[] args) throws Throwable {
                StringBuffer sb = new StringBuffer();
                sb.append(method.getName()); sb.append("(");
                for (int i=0; args != null && i<args.length; i++) {
                    if (i != 0)
                        sb.append(", ");
                    sb.append(args[i]);
                }
                sb.append(")");
                Object ret = method.invoke(underlying, args);
                if (ret != null) {
                    sb.append(" -> "); sb.append(ret);
                }
                System.out.println(sb);
                return ret;
            }
        }
    
    												
    										

    如果用日志代理包裝 HashSet,并執(zhí)行下面這個(gè)簡(jiǎn)單的測(cè)試程序:

    												
    														    Set s = newLoggingProxy(Set.class, new HashSet());
        s.add("three");
        if (!s.contains("four"))
            s.add("four");
        System.out.println(s);
    
    												
    										

    會(huì)得到以下輸出:

    												
    														  add(three) -> true
      contains(four) -> false
      add(four) -> true
      toString() -> [four, three]
      [four, three]
    
    												
    										

    這種方式是給對(duì)象添加調(diào)試包裝器的一種好的而且容易的方式。它當(dāng)然比生成代理類并手工創(chuàng)建大量 println() 語(yǔ)句容易得多(也更通用)。我進(jìn)一步改進(jìn)了這一方法;不必?zé)o條件地生成調(diào)試輸出,相反,代理可以查詢動(dòng)態(tài)配置存儲(chǔ)(從配置文件初始化,可以由 JMX MBean 動(dòng)態(tài)修改),確定是否需要生成調(diào)試語(yǔ)句,甚至可能在逐個(gè)類或逐個(gè)實(shí)例的基礎(chǔ)上進(jìn)行。

    在這一點(diǎn)上,我認(rèn)為讀者中的 AOP 愛好者們幾乎要跳出來說“這正是 AOP 擅長(zhǎng)的啊!”是的,但是解決問題的方法不止一種 —— 僅僅因?yàn)槟稠?xiàng)技術(shù)能解決某個(gè)問題,并不意味著它就是最好的解決方案。在任何情況下,動(dòng)態(tài)代理方式都有完全在“純 Java”范圍內(nèi)工作的優(yōu)勢(shì),不是每個(gè)公司都用(或應(yīng)當(dāng)用) AOP 的。

    動(dòng)態(tài)代理作為適配器

    代理也可以用作真正的適配器,提供了對(duì)象的一個(gè)視圖,導(dǎo)出與底層對(duì)象實(shí)現(xiàn)的接口不同的接口。調(diào)用句柄不需要把每個(gè)方法調(diào)用都分派給相同的底層對(duì)象;它可以檢查名稱,并把不同的方法分派給不同的對(duì)象。例如,假設(shè)有一組表示持久實(shí)體(PersonCompanyPurchaseOrder) 的 JavaBean 接口,指定了屬性的 getter 和 setter,而且正在編寫一個(gè)持久層,把數(shù)據(jù)庫(kù)記錄映射到實(shí)現(xiàn)這些接口的對(duì)象上。現(xiàn)在不用為每個(gè)接口編寫或生成類,可以只用一個(gè) JavaBean 風(fēng)格的通用代理類,把屬性保存在 Map 中。

    清單 7 顯示的動(dòng)態(tài)代理檢查被調(diào)用方法的名稱,并通過查詢或修改屬性圖直接實(shí)現(xiàn) getter 和 setter 方法。現(xiàn)在,這一個(gè)代理類就能實(shí)現(xiàn)多個(gè) JavaBean 風(fēng)格接口的對(duì)象。


    清單 7. 用于把 getter 和 setter 分派給 Map 的動(dòng)態(tài)代理類
    												
    																		
    public class JavaBeanProxyFactory {
        private static class JavaBeanProxy implements InvocationHandler {
            Map<String, Object> properties = new HashMap<String, 
              Object>();
    
            public JavaBeanProxy(Map<String, Object> properties) {
                this.properties.putAll(properties);
            }
    
            public Object invoke(Object proxy, Method method, 
              Object[] args) 
              throws Throwable {
                String meth = method.getName();
                if (meth.startsWith("get")) {
                    String prop = meth.substring(3);
                    Object o = properties.get(prop);
                    if (o != null && !method.getReturnType().isInstance(o))
                        throw new ClassCastException(o.getClass().getName() + 
                          " is not a " + method.getReturnType().getName());
                    return o;
                }
                else if (meth.startsWith("set")) {
                    // Dispatch setters similarly
                }
                else if (meth.startsWith("is")) {
                    // Alternate version of get for boolean properties
                }
                else {
                    // Can dispatch non get/set/is methods as desired
                }
            }
        }
    
        public static<T> T getProxy(Class<T> intf,
          Map<String, Object> values) {
            return (T) Proxy.newProxyInstance
              (JavaBeanProxyFactory.class.getClassLoader(),
                    new Class[] { intf }, new JavaBeanProxy(values));
        }
    }
    
    												
    										

    雖然因?yàn)榉瓷湓?Object 上工作會(huì)有潛在的類型安全性上的損失,但是,JavaBeanProxyFactory 中的 getter 處理會(huì)進(jìn)行一些必要的額外的類型檢測(cè),就像我在這里用 isInstance() 對(duì) getter 進(jìn)行的檢測(cè)一樣。





    回頁(yè)首


    性能成本

    正如已經(jīng)看到的,動(dòng)態(tài)代理?yè)碛泻?jiǎn)化大量代碼的潛力 —— 不僅能替代許多生成的代碼,而且一個(gè)代理類還能代替多個(gè)手寫的類或生成的代碼。什么是成本呢? 因?yàn)榉瓷涞胤峙煞椒ǘ皇遣捎脙?nèi)置的虛方法分派,可能有一些性能上的成本。在早期的 JDK 中,反射的性能很差(就像早期 JDK 中幾乎其他每件事的性能一樣),但是在近 10 年,反射已經(jīng)變得快多了。

    不必進(jìn)入基準(zhǔn)測(cè)試構(gòu)造的主題,我編寫了一個(gè)簡(jiǎn)單的、不太科學(xué)的測(cè)試程序,它循環(huán)地把數(shù)據(jù)填充到 Set,隨機(jī)地對(duì) Set進(jìn)行插入、查詢和刪除元素。我用三個(gè) Set 實(shí)現(xiàn)運(yùn)行它:一個(gè)未經(jīng)修飾的 HashSet,一個(gè)手寫的、只是把所有方法轉(zhuǎn)發(fā)到底層的 HashSetSet 適配器,還有一個(gè)基于代理的、也只是把所有方法轉(zhuǎn)發(fā)到底層 HashSetSet 適配器。每次循環(huán)迭代都生成若干隨機(jī)數(shù),并執(zhí)行一個(gè)或多個(gè) Set 操作。手寫的適配器比起原始的 HashSet 只產(chǎn)生很少百分比的性能負(fù)荷(大概是因?yàn)?JVM 級(jí)有效的內(nèi)聯(lián)緩沖和硬件級(jí)的分支預(yù)測(cè));代理適配器則明顯比原始 HashSet 慢,但是開銷要少于兩個(gè)量級(jí)。

    我從這個(gè)試驗(yàn)得出的結(jié)論是:對(duì)于大多數(shù)情況,代理方式即使對(duì)輕量級(jí)方法也執(zhí)行得足夠好,而隨著被代理的操作變得越來越重量級(jí)(例如遠(yuǎn)程方法調(diào)用,或者使用序列化、執(zhí)行 IO 或者從數(shù)據(jù)庫(kù)檢索數(shù)據(jù)的方法),代理開銷就會(huì)有效地接近于 0。當(dāng)然也存在一些代理方式的性能開銷無法接受的情況,但是這些通常只是少數(shù)情況。

    posted @ 2006-08-24 17:35 Binary 閱讀(213) | 評(píng)論 (0)編輯 收藏

    Java 理論與實(shí)踐: 偽 typedef 反模式

    將泛型添加到 Java? 語(yǔ)言中增加了類型系統(tǒng)的復(fù)雜性,提高了許多變量和方法聲明的冗長(zhǎng)程度。因?yàn)闆]有提供 “typedef” 工具來定義類型的簡(jiǎn)短名稱,所以有些開發(fā)人員轉(zhuǎn)而把擴(kuò)展當(dāng)作 “窮人的 typedef”,但是收到的決不是好的結(jié)果。在這個(gè)月的 Java 理論與實(shí)踐 中,Java 專家 Brian Goetz 解釋了這個(gè) “反模式” 的限制。

    對(duì)于 Java 5.0 中新增的泛型工具,一個(gè)常見的抱怨就是,它使代碼變得太冗長(zhǎng)。原來用一行就夠的變量聲明不再存在了,與聲明參數(shù)化類型有關(guān)的重復(fù)非常討厭,特別是還沒有良好地支持自動(dòng)補(bǔ)足的 IDE。例如,如果想聲明一個(gè) Map,它的鍵是 Socket,值是 Future<String>,那么老方法就是:

    												
    														Map socketOwner = new HashMap();
    
    												
    										

    比新方法緊湊得多:
    Map<Socket, Future<String>> socketOwner 
      = new HashMap<Socket, Future<String>>();  
    

    當(dāng)然,新方法內(nèi)置了更多類型信息,減少了編程錯(cuò)誤,提高了程序的可讀性,但是確實(shí)帶來了更多聲明變量和方法簽名方面的前期工作。類型參數(shù)在聲明和初始化中的重復(fù)看起來尤其沒有必要;SocketFuture<String> 需要輸入兩次,這迫使我們違犯了 “DRY” 原則(不要重復(fù)自己)。

    合成類似于 typedef 的東西

    添加泛型給類型系統(tǒng)增加了一些復(fù)雜性。在 Java 5.0 之前,“type” 和 “class” 幾乎是同義的,而參數(shù)化類型,特別是那些綁定的通配類型,使子類型和子類的概念有了顯著區(qū)別。類型 ArrayList<?>ArrayList<? extends Number>ArrayList<Integer> 是不同的類型,雖然它們是由同一個(gè)類 ArrayList 實(shí)現(xiàn)的。這些類型構(gòu)成了一個(gè)層次結(jié)構(gòu);ArrayList<?>ArrayList<? extends Number> 的超類型,而 ArrayList<? extends Number>ArrayList<Integer> 的超類型。

    對(duì)于原來的簡(jiǎn)單類型系統(tǒng),像 C 的 typedef 這樣的特性沒有意義。但是對(duì)于更復(fù)雜的類型系統(tǒng),typedef 工具可能會(huì)提供一些好處。不知是好還是壞,總之在泛型加入的時(shí)候,typedef 沒有加入 Java 語(yǔ)言。

    有些人用作 “窮人的 typedef” 的一個(gè)(壞的)做法是一個(gè)小小的擴(kuò)展:創(chuàng)建一個(gè)類,擴(kuò)展泛型類型,但是不添加功能,例如 SocketUserMap 類型,如清單 1 所示:


    清單 1. 偽 typedef 反模式 —— 不要這么做
    public class SocketUserMap extends HashMap<Socket<Future<String>> { }
    SocketUserMap socketOwner = new SocketUserMap();
    

    我將這個(gè)技巧稱為偽 typedef 反模式,它實(shí)現(xiàn)了將 socketOwner 定義簡(jiǎn)化為一行的這一(有問題的)目標(biāo),但是有些副作用,最終成為重用和維護(hù)的障礙。(對(duì)于有明確的構(gòu)造函數(shù)而不是無參構(gòu)造函數(shù)的類來說,派生類也需要聲明每個(gè)構(gòu)造函數(shù),因?yàn)闃?gòu)造函數(shù)沒有被繼承。)





    回頁(yè)首


    偽類型的問題

    在 C 中,用 typedef 定義一個(gè)新類型更像是宏,而不是類型聲明。定義等價(jià)類型的 typedef,可以與原始類型自由地互換。清單 2 顯示了一個(gè)定義回調(diào)函數(shù)的示例,其中在簽名中使用了一個(gè) typedef,但是調(diào)用者提供給回調(diào)的是一個(gè)等價(jià)類型,而編譯器和運(yùn)行時(shí)都可以接受它:


    清單 2. C 語(yǔ)言的 typedef 示例
    // Define a type called "callback" that is a function pointer
    typedef void (*Callback)(int);
    
    void doSomething(Callback callback) { }
    
    // This function conforms to the type defined by Callback
    void callbackFunction(int arg) { }
    
    // So a caller can pass the address of callbackFunction to doSomething
    void useCallback() {
      doSomething(&callbackFunction); 
    }
    

    擴(kuò)展不是類型定義

    用 Java 語(yǔ)言編寫的試圖使用偽 typedef 的等價(jià)程序就會(huì)出現(xiàn)麻煩。清單 3 的 StringListUserList 類型都擴(kuò)展了一個(gè)公共超類,但是它們不是等價(jià)的類型。這意味著任何想調(diào)用 lookupAll 的代碼都必須傳遞一個(gè) StringList,而不能是 List<String>UserList


    清單 3. 偽類型如何把客戶限定在只能使用偽類型
    class StringList extends ArrayList<String> { }
    class UserList extends ArrayList<String> { }
    ...
    class SomeClass {
        public void validateUsers(UserList users) { ... }
        public UserList lookupAll(StringList names) { ... }
    }
    

    這個(gè)限制要比初看上去嚴(yán)格得多。在小程序中,可能不會(huì)有太大差異,但是當(dāng)程序變大的時(shí)候,使用偽類型的需求就會(huì)不斷地造成問題。如果變量類型是 StringList,就不能給它分配普通的 List<String>,因?yàn)?List<String>StringList 的超類型,所以不是 StringList。就像不能把 Object 分配給類型為 String 的變量一樣,也不能把 List<String> 分配給類型為 StringList 的變量(但是,可以反過來,例如,可以把 StringList 分配給類型為 List<String> 的變量,因?yàn)?List<String>StringList 的超類型。)

    同樣的情況也適用于方法的參數(shù);如果一個(gè)方法參數(shù)是 StringList 類型,那么就不能把普通的 List<String> 傳遞給它。這意味著,如果不要求這個(gè)方法的每次使用都使用偽類型,那么根本不能用偽類型作為方法參數(shù),而這在實(shí)踐當(dāng)中就意味著在庫(kù) API 中根本就不能使用偽類型。而且大多數(shù)庫(kù) API 都源自本來沒想成為庫(kù)代碼的那些代碼,所以 “這個(gè)代碼只是給我自己的,沒有其他人會(huì)用它” 可不是個(gè)好借口(只要您的代碼有一點(diǎn)兒用處,別人就有可能會(huì)使用它;如果您的代碼臭得很,那您可能是對(duì)的)。

    偽類型會(huì)傳染

    這種 “病毒” 性質(zhì)是讓 C 代碼的重用有困難的因素之一。差不多每個(gè) C 包都有頭文件,定義工具宏和類型,像 int32booleantruefalse,諸如此類。如果想在一個(gè)應(yīng)用程序內(nèi)使用幾個(gè)包,而它們對(duì)于這些公共條目沒有使用相同的定義,那么即使要編譯一個(gè)只包含所有頭文件的空程序,之前也要在 “頭文件地獄” 問題上花好長(zhǎng)時(shí)間。如果編寫的 C 應(yīng)用程序要使用許多來自不同作者的不同的包,那么幾乎肯定要涉及一些這類痛苦。另一方面,對(duì)于 Java 應(yīng)用程序來說,在沒有這類痛苦的情況下使用許多甚至更多的包,是非常常見的事。如果包要在它們的 API 中使用偽類型,那么我們可能就要重新經(jīng)歷早已留在痛苦回憶中的問題。

    作為示例,假設(shè)有兩個(gè)不同的包,每個(gè)包都用偽類型反模式定義了 StringList,如清單 4 所示,而且每個(gè)包都定義了操作 StringList 的工具方法。兩個(gè)包都定義了同樣的標(biāo)識(shí)符,這一事實(shí)已經(jīng)是不方便的一個(gè)小源頭了;客戶程序必須選擇導(dǎo)入一個(gè)定義,而另一個(gè)定義則要使用完全限定的名稱。但是更大的問題是現(xiàn)在這些包的客戶無法創(chuàng)建既能傳遞給 sortList 又能傳遞給 reverseList 的對(duì)象,因?yàn)閮蓚€(gè)不同的 StringList 類型是不同的類型,彼此互不兼容。客戶現(xiàn)在必須在使用一個(gè)包還是使用另一個(gè)包之間進(jìn)行選擇,否則他們就必須做許多工作,在不同類型的 StringList 之間進(jìn)行轉(zhuǎn)換。對(duì)包的作者來說以為方便的東西,成為在所有地方使用這個(gè)包的突出障礙,除非在最受限的環(huán)境中。


    清單 4. 偽類型的使用如何妨礙重用
    package a;
    
    class StringList extends ArrayList<String> { }
    class ListUtilities {
        public static void sortList(StringList list) { }
    }
    
    package b;
    
    class StringList extends ArrayList<String> { }
    class SomeOtherUtilityClass {
        public static void reverseList(StringList list) { }
    }
     
    ...
    
    class Client {
        public void someMethod() {
            StringList list = ...;
            // Can't do this
            ListUtilities.sortList(list);
            SomeOtherUtilityClass.reverseList(list);
        }
    }
    

    偽類型通常太具體

    偽類型反模式的進(jìn)一步問題是,它會(huì)喪失使用接口定義變量類型和方法參數(shù)的好處。雖然可以把 StringList 定義成擴(kuò)展 List<String> 的接口,再定義一個(gè)具體類型 StringArrayList 來擴(kuò)展 ArrayList<String> 并實(shí)現(xiàn) StringList,但多數(shù)偽 typedef 反模式的用戶通常達(dá)不到這種水平,因?yàn)檫@項(xiàng)技術(shù)的目的主要是為了簡(jiǎn)化和縮短類型的名稱。但結(jié)果是,API 的用處減少了并變得更脆弱,因?yàn)樗鼈兪褂?ArrayList 這樣的具體類型,而不是 List 這樣的抽象類型。

    更安全的技巧

    一個(gè)更安全的減少聲明泛型集合所需打字量的技巧是使用類型推導(dǎo)(type inference)。編譯器可以非常聰明地使用程序中內(nèi)嵌的類型信息來分配類型參數(shù)。如果定義了下面這樣一個(gè)工具方法:

    public static <K,V> Map<K,V> newHashMap() {
        return new HashMap<K,V>(); 
    }
    

    那么可以安全地用它來避免錄入兩次參數(shù):
    Map<Socket, Future<String>> socketOwner = Util.newHashMap();
    

    這種方法之所以能夠奏效,在于編譯器可以根據(jù)泛型方法 newHashMap() 被調(diào)用的位置推導(dǎo)出 KV 的值。



    回頁(yè)首


    結(jié)束語(yǔ)

    偽 typedef 反模式的動(dòng)機(jī)很簡(jiǎn)單 —— 開發(fā)人員想要一種方法可以定義更緊湊的類型標(biāo)識(shí)符,特別是在泛型把類型標(biāo)識(shí)符變得更冗長(zhǎng)的時(shí)候。問題在于這個(gè)做法在使用它的代碼和代碼的客戶之間形成了緊密的耦合,從而妨礙了重用。不喜歡泛型類型標(biāo)識(shí)符的冗長(zhǎng)是可以理解的,但這不是解決問題的辦法。

    posted @ 2006-08-24 17:34 Binary 閱讀(156) | 評(píng)論 (0)編輯 收藏

    僅列出標(biāo)題
    共8頁(yè): 上一頁(yè) 1 2 3 4 5 6 7 8 下一頁(yè) 
    主站蜘蛛池模板: 午夜无遮挡羞羞漫画免费| 91禁漫免费进入| 人妻丰满熟妇无码区免费| 99精品视频免费在线观看| 67194成是人免费无码| 午夜免费福利影院| 亚洲电影免费观看| 亚洲人成人网站18禁| 亚洲a∨无码一区二区| 色多多A级毛片免费看| 久久精品国产影库免费看| 人妻仑刮八A级毛片免费看| 国产精品无码亚洲精品2021| 国产成人va亚洲电影| 国产免费一级高清淫曰本片| 每天更新的免费av片在线观看| 丁香花免费高清视频完整版| 国产美女无遮挡免费网站| 精品国产人成亚洲区| 亚洲成人免费在线观看| 亚洲一本到无码av中文字幕| 国产成人午夜精品免费视频| 亚洲手机中文字幕| 国产一卡二卡四卡免费| 中文字幕第13亚洲另类| xxxxxx日本处大片免费看| 亚洲成在人线aⅴ免费毛片| 亚洲成人动漫在线观看| 国产精品成人观看视频免费| 亚洲精品在线网站| 久久99精品视免费看| 久久精品视频亚洲| 亚洲中文无码线在线观看| 99精品视频免费| 免费国产黄线在线观看| 国产精品亚洲综合久久| 午夜电影免费观看| 亚洲午夜福利在线视频| 99视频在线精品免费观看6| 久久精品亚洲日本波多野结衣| 免费a级黄色毛片|