<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, 評論 - 5, 引用 - 0
    數據加載中……

    Java理論與實踐: 它是誰的對象?

    在沒有垃圾收集的語言中,比如C++,必須特別關注內存管理。對于每個動態對象,必須要么實現引用計數以模擬 垃圾收集效果,要么管理每個對象的“所有權”――確定哪個類負責刪除一個對象。通常,對這種所有權的維護并沒有什么成文的規則,而是按照約定(通常是不成文的)進行維護。盡管垃圾收集意味著Java開發者不必太多地擔心內存 泄漏,有時我們仍然需要擔心對象所有權,以防止數據爭用(data races)和不必要的副作用。在這篇文章中,Brian Goetz 指出了一些這樣的情況,即Java開發者必須注意對象所有權。請在 論壇上與作者及其他讀者共享您對本文的一些想法(您也可以在文章的頂部或底部點擊 討論來訪問論壇)。

    如果您是在1997年之前開始學習編程,那么可能您學習的第一種編程語言沒有提供透明的垃圾收集。每一個new 操作必須有相應的delete操作 ,否則您的程序就會泄漏內存,最終內存分配器(memory allocator )就會出故障,而您的程序就會崩潰。每當利用 new 分配一個對象時,您就得問自己,誰將刪除該對象?何時刪除?

    別名, 也叫做 ...

    內存管理復雜性的主要原因是別名使用:同一塊內存或對象具有 多個指針或引用。別名在任何時候都會很自然地出現。例如,在清單 1 中,在 makeSomething 的第一行創建的 Something 對象至少有四個引用:

    • something 引用。
    • 集合 c1 中至少有一個引用。
    • 當 something 被作為參數傳遞給 registerSomething 時,會創建臨時 aSomething 引用。
    • 集合 c2 中至少有一個引用。

    清單 1. 典型代碼中的別名
    												
    														    Collection c1, c2;
        
        public void makeSomething {
            Something something = new Something();
            c1.add(something);
            registerSomething(something);
        }
    
        private void registerSomething(Something aSomething) {
            c2.add(aSomething);
        }
    
    												
    										

    在非垃圾收集語言中需要避免兩個主要的內存管理危險:內存泄漏和懸空指針。為了防止內存泄漏,必須確保每個分配了內存的對象最終都會被刪除。 為了避免懸空指針(一種危險的情況,即一塊內存已經被釋放了,而一個指針還在引用它),必須在最后的引用釋放之后才刪除對象。為滿足這兩條約束,采用一定的策略是很重要的。

    為內存管理而管理對象所有權
    除了垃圾收集之外,通常還有其他兩種方法用于處理別名問題: 引用計數和所有權管理。引用計數(reference counting)是對一個給定的對象當前有多少指向它的引用保留有一個計數,然后當最后一個引用被釋放時自動刪除該對象。在 C和20世紀90年代中期之前的多數 C++ 版本中,這是不可能自動完成的。標準模板庫(Standard Template Library,STL)允許創建“靈巧”指針,而不能自動實現引用計數(要查看一些例子,請參見開放源代碼 Boost 庫中的 shared_ptr 類,或者參見STL中的更加簡單的 auto_ptr 類)。

    所有權管理(ownership management) 是這樣一個過程,該過程指明一個指針是“擁有”指針("owning" pointer),而 所有其他別名只是臨時的二類副本( temporary second-class copies),并且只在所擁有的指針被釋放時才刪除對象。在有些情況下,所有權可以從一個指針“轉移”到另一個指針,比如一個這樣的方法,它以一個緩沖區作為參數,該方法用于向一個套接字寫數據,并且在寫操作完成時刪除這個緩沖區。這樣的方法通常叫做接收器 (sinks)。在這個例子中,緩沖區的所有權已經被有效地轉移,因而進行調用的代碼必須假設在被調用方法返回時緩沖區已經被刪除。(通過確保所有的別名指針都具有與調用堆棧(比如方法參數或局部變量)一致的作用域(scope ),可以進一步簡化所有權管理,如果引用將由非堆棧作用域的變量保存,則通過復制對象來進行簡化。)





    回頁首


    那么,怎么著?

    此時,您可能正納悶,為什么我還要討論內存管理、別名和對象所有權。畢竟,垃圾收集是 Java語言的核心特性之一,而內存管理是已經過時的一件麻煩事。就讓垃圾收集器來處理這件事吧,這正是它的工作。那些從內存管理的麻煩中解脫出來的人不愿意再回到過去,而那些從未處理過內存管理的人則根本無法想象在過去倒霉的日子里――比如1996年――程序員的編程是多么可怕。





    回頁首


    提防懸空別名

    那么這意味著我們可以與對象所有權的概念說再見了嗎?可以說是,也可以說不是。 大多數情況下,垃圾收集確實消除了顯式資源存儲單元分配(explicit resource deallocation)的必要(在以后的專欄中我將討論一些例外)。但是,有一個區域中,所有權管理仍然是Java 程序中的一個問題,而這就是懸空別名(dangling aliases)問題。 Java 開發者通常依賴于這樣一個隱含的假設,即假設由對象所有權來確定哪些引用應該被看作是只讀的 (在C++中就是一個 const 指針),哪些引用可以用來修改被引用的對象的狀態。當兩個類都(錯誤地)認為自己保存有對給定對象的惟一可寫的引用時,就會出現懸空指針。發生這種情況時,如果對象的狀態被意外地更改,這兩個類中的一個或兩者將會產生混淆。

    一個貼切的例子

    考慮清單 2 中的代碼,其中的 UI 組件保存有一個 Point 對象,用于表示它的位置。當調用 MathUtil.calculateDistance 來計算對象移動了多遠時,我們依賴于一個隱含而微妙的假設――即 calculateDistance 不會改變傳遞給它的 Point 對象的狀態,或者情況更壞,維護著對那些 Point 對象的一個引用(比如通過將它們保存在集合中或者將它們傳遞到另一個線程),然后這個引用將用于在 calculateDistance 返回后更改Point 對象的狀態。 在 calculateDistance的例子中,為這種行為擔心似乎有些可笑,因為這明顯是一個可怕的違背慣例的情況。但是,如果要說將一個可變的對象傳遞給一個方法,之后對象還能夠毫發無損地返回來,并且將來對于對象的狀態也不會有不可預料的副作用(比如該方法與另一個線程共享引用,該線程可能會等待5分鐘,然后更改對象的狀態),那么這只不過是一廂情愿的想法而已。


    清單 2. 將可變對象傳遞給外部方法是不可取的
    												
    														    private Point initialLocation, currentLocation;
    
        public Widget(Point initialLocation) {
            this.initialLocation = initialLocation;
            this.currentLocation = initialLocation;
        }
    
        public double getDistanceMoved() {
            return MathUtil.calculateDistance(initialLocation, currentLocation);
        }
        
        . . . 
    
        // The ill-behaved utility class MathUtil
        public static double calculateDistance(Point p1, 
                                               Point p2) {
            double distance = Math.sqrt((p2.x - p1.x) ^ 2 
                                        + (p2.y - p1.y) ^ 2);
            p2.x = p1.x;
            p2.y = p1.y;
            return distance;
        }
    
    												
    										

    一個愚蠢的例子

    大家對該例子明顯而普遍的反應就是――這是一個愚蠢的例子――只是強調了這樣一個事實,即對象所有權的概念在 Java 程序中依然存在,而且存在得很好,只是沒有說明而已。calculateDistance 方法不應該改變它的參數的狀態,因為它并不“擁有”它們――當然,調用方法擁有它們。因此說不用考慮對象所有權。

    下面是一個更加實用的例子,它說明了不知道誰擁有對象就有可能會引起混淆。再次考慮一個以Point 屬性 來表示其位置的 UI組件。 清單 3 顯示了實現存取器方法 setLocation 和 getLocation的三種方式。第一種方式是最懶散的,并且提供了最好的性能,但是對于蓄意攻擊和無意識的失誤,它有幾個薄弱環節。


    清單 3. getters 和 setters的值語義以及引用語義
    												
    														public class Widget {
        private Point location;
    
        // Version 1: No copying -- getter and setter implement reference 
        // semantics
        // This approach effectively assumes that we are transferring 
        // ownership of the Point from the caller to the Widget, but this 
        // assumption is rarely explicitly documented. 
        public void setLocation(Point p) {
            this.location = p;
        }
    
        public Point getLocation() {
            return location;
        }
    
        // Version 2: Defensive copy on setter, implementing value 
        // semantics for the setter
        // This approach effectively assumes that callers of 
        // getLocation will respect the assumption that the Widget 
        // owns the Point, but this assumption is rarely documented.
        public void setLocation(Point p) {
            this.location = new Point(p.x, p.y);
        }
    
        public Point getLocation() {
            return location;
        }
    
        // Version 3: Defensive copy on getter and setter, implementing 
        // true value semantics, at a performance cost
        public void setLocation(Point p) {
            this.location = new Point(p.x, p.y);
        }
    
        public Point getLocation() {
            return (Point) location.clone();
        }
    }
    
    												
    										

    現在來考慮 setLocation 看起來是無意的使用 :

    												
    														    Widget w1, w2;
        . . . 
        Point p = new Point();
        p.x = p.y = 1;
        w1.setLocation(p);
        
        p.x = p.y = 2;
        w2.setLocation(p);
    
    												
    										

    或者是:

    												
    														    w2.setLocation(w1.getLocation());
    
    												
    										

    在setLocation/getLocation存取器實現的版本 1 之下,可能看起來好像第一個Widget的 位置是 (1, 1) ,第二個Widget的位置是 (2, 2),而事實上,二者都是 (2, 2)。這可能對于調用者(因為第一個Widget意外地移動了)和Widget 類(因為它的位置改變了,而與Widget代碼無關)來說都會產生混淆。在第二個例子中,您可能認為自己只是將Widget w2移動到 Widget w1當前所在的位置 ,但是實際上您這樣做便規定了每次w1 移動時w2都跟隨w1 。

    防御性副本

    setLocation 的版本 2 做得更好:它創建了傳遞給它的參數的一個副本,以確保不存在可以意外改變其狀態的 Point的別名。但是它也并非無可挑剔,因為下面的代碼也將具有一個很可能不希望出現的效果,即Widget在不知情的情況下被移動了:

    												
    														    Point p = w1.getLocation();
        . . .
        p.x = 0;
    
    												
    										

    getLocation 和 setLocation 的版本 3 對于別名引用的惡意或無意使用是完全安全的。這一安全是以一些性能為代價換來的:每次調用一個 getter 或 setter 都會創建一個新對象。

    getLocation 和 setLocation 的不同版本具有不同的語義,通常這些語義被稱作值語義(版本 1)和引用語義(版本 3)。不幸的是,通常沒有說明實現者應該使用的是哪種語義。結果,這個類的使用者并不清楚這一點,從而作出了更差的假設(即選擇了不是最合適的語義)。

    getLocation 和 setLocation 的版本 3 所使用的技術叫做防御性復制( defensive copying),盡管存在著明顯的性能上的代價,您也應該養成這樣的習慣,即幾乎每次返回和存儲對可變對象或數組的引用時都使用這一技術,尤其是在您編寫一個通用的可能被不是您自己編寫的代碼調用(事實上這很常見)的工具時更是如此。有別名的可變對象被意外修改的情況會以許多微妙且令人驚奇的方式突然出現,并且調試起來相當困難。

    而且情況還會變得更壞。假設您是Widget類的一個使用者,您并不知道存取器具有值語義還是引用語義。 謹慎的做法是,在調用存取器方法時也使用防御性副本。所以,如果您想要將 w2 移動到 w1 的當前位置,您應該這樣去做:

    												
    														    Point p = w1.getLocation();
        w2.setLocation(new Point(p.x, p.y));
    
    												
    										

    如果 Widget 像在版本 2 或 3 中一樣實現其存取器,那么我們將為每個調用創建兩個臨時對象 ――一個在 setLocation 調用的外面,一個在里面。

    文檔說明存取器語義

    getLocation 和 setLocation 的版本 1 的真正問題不是它們易受混淆別名副作用的不良影響(確實是這樣),而是它們的語義沒有清楚的說明。如果存取器被清楚地說明為具有引用語義(而不是像通常那樣被假設為值語義),那么調用者將更可能認識到,在它們調用setLocation時,它們是將Point對象的所有權轉移給另一個實體,并且也不大可能仍然認為它們還擁有Point對象的所有權,因而還能夠再次使用它。





    回頁首


    利用不可改變性解決以上問題

    如果一開始就使得Point 成為不可變的,那么這些與 Point 有關的問題早就迎刃而解了。不可變對象上沒有副作用,并且緩存不可變對象的引用總是安全的,不會出現別名問題。如果 Point是不可變的,那么與setLocation 和 getLocation存取器的語義有關的所有問題都是非常確定的 。不可變屬性的存取器將總是具有值引用,因而調用的任何一方都不需要防御性復制,這使得它們效率更高。

    那么為什么不在一開始就使得Point 成為不可變的呢?這可能是出于性能上的原因,因為早期的 JVM具有不太有效的垃圾收集器。 那時,每當一個對象(甚至是鼠標)在屏幕上移動就創建一個新的Point的對象創建開銷可能有些讓人生畏,而創建防御性副本的開銷則不在話下。

    依后見之明,使Point成為可變的這個決定被證明對于程序清晰性和性能是昂貴的代價。Point類的可變性使得每一個接受Point作為參數或者要返回一個Point的方法背上了編寫文檔說明的沉重負擔。也就是說,它得說明它是要改變Point,還是在返回之后保留對Point的一個引用。因為很少有類真正包含這樣的文檔,所以在調用一個沒有用文檔說明其調用語義或副作用行為的方法時,安全的策略是在傳遞它到任何這樣的方法之前創建一份防御副本。

    有諷刺意味的是,使 Point成為可變的這個決定所帶來的性能優勢被由于Point的可變性而需要進行的防御性復制給抵消了。由于缺乏清晰的文檔說明(或者缺少信任),在方法調用的兩邊都需要創建防御副本 ――調用者需要這樣做是因為它不知道被調用者是否會粗暴地改變 Point,而被調用者需要這樣做是因為它不知道是否保留了對 Point 的引用。





    回頁首


    一個現實的例子

    下面是懸空別名問題的另一個例子,該例子非常類似于我最近在一個服務器應用中所看到的。 該應用在內部使用了發布-訂閱式消息傳遞方式,以將事件和狀態更新傳達到服務器內的其他代理。這些代理可以訂閱任何一個它們感興趣的消息流。一旦發布之后,傳遞到其他代理的消息就可能在將來某個時候在一個不同的線程中被處理。

    清單 4 顯示了一個典型的消息傳遞事件(即發布拍賣系統中一個新的高投標通知)和產生該事件的代碼。不幸的是,消息傳遞事件實現和調用者實現的交互合起來創建了一個懸空別名。通過簡單地復制而不是克隆數組引用,消息和產生消息的類都保存了前一投標數組的主副本的一個引用。如果消息發布時的時間和消費時的時間有任何延遲,那么訂閱者看到的 previous5Bids 數組的值將不同于消息發布時的時間,并且多個訂閱者看到的前面投標的值可能會互不相同。在這個例子中,訂閱者將看到當前投標的歷史值和前面投標的更接近現在的值,從而形成了這樣的錯覺,認為前面投標比當前投標的值要高。不難設想這將如何引起問題――這還不算,當應用在很大的負載下時,這樣一個問題則更是暴露無遺。 使得消息類不可變并在構造時克隆像數組這樣的可變引用,就可以防止該問題。


    清單 4. 發布-訂閱式消息傳遞代碼中的懸空數組別名
    												
    														public interface MessagingEvent { ... }
    
    public class CurrentBidEvent implements MessagingEvent { 
      public final int currentBid;
      public final int[] previous5Bids;
    
      public CurrentBidEvent(int currentBid, int[] previousBids) {
        this.currentBid = currentBid;
        // Danger -- copying array reference instead of values
        this.previous5Bids = previous5Bids;
      }
    
      ...
    }
    
      // Now, somewhere in the bid-processing code, we create a 
      // CurrentBidEvent and publish it.  
      public void newBid(int newBid) { 
        if (newBid > currentBid) { 
          for (int i=1; i<5; i++) 
            previous5Bids[i] = previous5Bids[i-1];
          previous5Bids[0] = currentBid;
          currentBid = newBid;
    
          messagingTopic.publish(new CurrentBidEvent(currentBid, previousBids));
        }
      }
    }
    
    												
    										





    回頁首


    可變對象的指導

    如果您要創建一個可變類 M,那么您應該準備編寫比 M 是不可變的情況下多得多的文檔說明,以說明怎樣處理 M 的引用。 首先,您必須選擇以 M 為參數或返回 M 對象的方法是使用值語義還是引用語義,并準備在每一個在其接口內使用 M 的其他類中清晰地文檔說明這一點 。如果接受或返回 M 對象的任何方法隱式地假設 M 的所有權被轉移,那么您必須也文檔說明這一點。您還要準備著接受在必要時創建防御副本的性能開銷。

    一個必須處理對象所有權問題的特殊情況是數組,因為數組不可以是不可變的。當傳遞一個數組引用到另一個類時,可能有創建防御副本的代價,除非您能確保其他類要么創建了它自己的副本,要么只在調用期間保存引用,否則您可能需要在傳遞數組之前創建副本。另外,您可以容易地結束這樣一種情形,即調用的兩邊的類都隱式地假設它們擁有數組,只是這樣會有不可預知的結果出現。

    posted @ 2006-08-24 17:33 Binary 閱讀(276) | 評論 (0)編輯 收藏

    輕松使用線程: 不共享有時是最好的

    ThreadLocal 類是悄悄地出現在 Java 平臺版本 1.2 中的。雖然支持線程局部變量早就是許多線程工具(例如 Posix pthreads 工具)的一部分,但 Java Threads API 的最初設計卻沒有這項有用的功能。而且,最初的實現也相當低效。由于這些原因, ThreadLocal 極少受到關注,但對簡化線程安全并發程序的開發來說,它卻是很方便的。在 輕松使用線程的第 3 部分,Java 軟件顧問 Brian Goetz 研究了 ThreadLocal 并提供了一些使用技巧。

    參加 Brian 的 多線程 Java 編程討論論壇以獲得您工程中的線程和并發問題的幫助。

    編寫線程安全類是困難的。它不但要求仔細分析在什么條件可以對變量進行讀寫,而且要求仔細分析其它類能如何使用某個類。 有時,要在不影響類的功能、易用性或性能的情況下使類成為線程安全的是很困難的。有些類保留從一個方法調用到下一個方法調用的狀態信息,要在實踐中使這樣的類成為線程安全的是困難的。

    管理非線程安全類的使用比試圖使類成為線程安全的要更容易些。非線程安全類通常可以安全地在多線程程序中使用,只要您能確保一個線程所用的類的實例不被其它線程使用。例如,JDBC Connection 類是非線程安全的 — 兩個線程不能在小粒度級上安全地共享一個 Connection — 但如果每個線程都有它自己的 Connection ,那么多個線程就可以同時安全地進行數據庫操作。

    不使用 ThreadLocal 為每個線程維護一個單獨的 JDBC 連接(或任何其它對象)當然是可能的;Thread API 給了我們把對象和線程聯系起來所需的所有工具。而 ThreadLocal 則使我們能更容易地把線程和它的每線程(per-thread)數據成功地聯系起來。

    什么是線程局部變量(thread-local variable)?

    線程局部變量高效地為每個使用它的線程提供單獨的線程局部變量值的副本。每個線程只能看到與自己相聯系的值,而不知道別的線程可能正在使用或修改它們自己的副本。一些編譯器(例如 Microsoft Visual C++ 編譯器或 IBM XL FORTRAN 編譯器)用存儲類別修飾符(像 staticvolatile )把對線程局部變量的支持集成到了其語言中。Java 編譯器對線程局部變量不提供特別的語言支持;相反地,它用 ThreadLocal 類實現這些支持, 核心 Thread 類中有這個類的特別支持。

    因為線程局部變量是通過一個類來實現的,而不是作為 Java 語言本身的一部分,所以 Java 語言線程局部變量的使用語法比內建線程局部變量語言的使用語法要笨拙一些。要創建一個線程局部變量,請實例化類 ThreadLocal 的一個對象。 ThreadLocal 類的行為與 java.lang.ref 中的各種 Reference 類的行為很相似; ThreadLocal 類充當存儲或檢索一個值時的間接句柄。清單 1 顯示了 ThreadLocal 接口。


    清單 1. ThreadLocal 接口
    												
    														public class ThreadLocal { 
      public Object get();
      public void set(Object newValue);
      public Object initialValue();
    }
    
    												
    										

    get() 訪問器檢索變量的當前線程的值; set() 訪問器修改當前線程的值。 initialValue() 方法是可選的,如果線程未使用過某個變量,那么您可以用這個方法來設置這個變量的初始值;它允許延遲初始化。用一個示例實現來說明 ThreadLocal 的工作方式是最好的方法。清單 2 顯示了 ThreadLocal 的一個實現方式。它不是一個特別好的實現(雖然它與最初實現非常相似),所以很可能性能不佳,但它清楚地說明了 ThreadLocal 的工作方式。


    清單 2. ThreadLocal 的糟糕實現
    												
    														public class ThreadLocal { 
      private Map values = Collections.synchronizedMap(new HashMap());
    
      public Object get() {
        Thread curThread = Thread.currentThread();
        Object o = values.get(curThread);
        if (o == null && !values.containsKey(curThread)) {
          o = initialValue();
          values.put(curThread, o);
        }
        return o;
      }
    
      public void set(Object newValue) {
        values.put(Thread.currentThread(), newValue);
      }
    
      public Object initialValue() {
        return null;
      }
    }
    
    												
    										

    這個實現的性能不會很好,因為每個 get()set() 操作都需要 values 映射表上的同步,而且如果多個線程同時訪問同一個 ThreadLocal ,那么將發生爭用。此外,這個實現也是不切實際的,因為用 Thread 對象做 values 映射表中的關鍵字將導致無法在線程退出后對 Thread 進行垃圾回收,而且也無法對死線程的 ThreadLocal 的特定于線程的值進行垃圾回收。





    回頁首


    用 ThreadLocal 實現每線程 Singleton

    線程局部變量常被用來描繪有狀態“單子”(Singleton) 或線程安全的共享對象,或者是通過把不安全的整個變量封裝進 ThreadLocal ,或者是通過把對象的特定于線程的狀態封裝進 ThreadLocal 。例如,在與數據庫有緊密聯系的應用程序中,程序的很多方法可能都需要訪問數據庫。在系統的每個方法中都包含一個 Connection 作為參數是不方便的 — 用“單子”來訪問連接可能是一個雖然更粗糙,但卻方便得多的技術。然而,多個線程不能安全地共享一個 JDBC Connection 。如清單 3 所示,通過使用“單子”中的 ThreadLocal ,我們就能讓我們的程序中的任何類容易地獲取每線程 Connection 的一個引用。這樣,我們可以認為 ThreadLocal 允許我們創建 每線程單子。


    清單 3. 把一個 JDBC 連接存儲到一個每線程 Singleton 中
    												
    														public class ConnectionDispenser { 
      private static class ThreadLocalConnection extends ThreadLocal {
        public Object initialValue() {
          return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
        }
      }
    
      private ThreadLocalConnection conn = new ThreadLocalConnection();
    
      public static Connection getConnection() {
        return (Connection) conn.get();
      }
    }
    
    												
    										

    任何創建的花費比使用的花費相對昂貴些的有狀態或非線程安全的對象,例如 JDBC Connection 或正則表達式匹配器,都是可以使用每線程單子(singleton)技術的好地方。當然,在類似這樣的地方,您可以使用其它技術,例如用池,來安全地管理共享訪問。然而,從可伸縮性角度看,即使是用池也存在一些潛在缺陷。因為池實現必須使用同步,以維護池數據結構的完整性,如果所有線程使用同一個池,那么在有很多線程頻繁地對池進行訪問的系統中,程序性能將因爭用而降低。





    回頁首


    用 ThreadLocal 簡化調試日志紀錄

    其它適合使用 ThreadLocal 但用池卻不能成為很好的替代技術的應用程序包括存儲或累積每線程上下文信息以備稍后檢索之用這樣的應用程序。例如,假設您想創建一個用于管理多線程應用程序調試信息的工具。您可以用如清單 4 所示的 DebugLogger 類作為線程局部容器來累積調試信息。在一個工作單元的開頭,您清空容器,而當一個錯誤出現時,您查詢該容器以檢索這個工作單元迄今為止生成的所有調試信息。


    清單 4. 用 ThreadLocal 管理每線程調試日志
    												
    														public class DebugLogger {
      private static class ThreadLocalList extends ThreadLocal {
        public Object initialValue() {
          return new ArrayList();
        }
    
        public List getList() { 
          return (List) super.get(); 
        }
      }
    
      private ThreadLocalList list = new ThreadLocalList();
      private static String[] stringArray = new String[0];
    
      public void clear() {
        list.getList().clear();
      }
    
      public void put(String text) {
        list.getList().add(text);
      }
    
      public String[] get() {
        return list.getList().toArray(stringArray);
      }
    }
    
    												
    										

    在您的代碼中,您可以調用 DebugLogger.put() 來保存您的程序正在做什么的信息,而且,稍后如果有必要(例如發生了一個錯誤),您能夠容易地檢索與某個特定線程相關的調試信息。 與簡單地把所有信息轉儲到一個日志文件,然后努力找出哪個日志記錄來自哪個線程(還要擔心線程爭用日志紀錄對象)相比,這種技術簡便得多,也有效得多。

    ThreadLocal 在基于 servlet 的應用程序或工作單元是一個整體請求的任何多線程應用程序服務器中也是很有用的,因為在處理請求的整個過程中將要用到單個線程。您可以通過前面講述的每線程單子技術用 ThreadLocal 變量來存儲各種每請求(per-request)上下文信息。





    回頁首


    ThreadLocal 的線程安全性稍差的堂兄弟,InheritableThreadLocal

    ThreadLocal 類有一個親戚,InheritableThreadLocal,它以相似的方式工作,但適用于種類完全不同的應用程序。創建一個線程時如果保存了所有 InheritableThreadLocal 對象的值,那么這些值也將自動傳遞給子線程。如果一個子線程調用 InheritableThreadLocalget() ,那么它將與它的父線程看到同一個對象。為保護線程安全性,您應該只對不可變對象(一旦創建,其狀態就永遠不會被改變的對象)使用 InheritableThreadLocal ,因為對象被多個線程共享。 InheritableThreadLocal 很合適用于把數據從父線程傳到子線程,例如用戶標識(user id)或事務標識(transaction id),但不能是有狀態對象,例如 JDBC Connection 。





    回頁首


    ThreadLocal 的性能

    雖然線程局部變量早已赫赫有名并被包括 Posix pthreads 規范在內的很多線程框架支持,但最初的 Java 線程設計中卻省略了它,只是在 Java 平臺的版本 1.2 中才添加上去。在很多方面, ThreadLocal 仍在發展之中;在版本 1.3 中它被重寫,版本 1.4 中又重寫了一次,兩次都專門是為了性能問題。

    在 JDK 1.2 中, ThreadLocal 的實現方式與清單 2 中的方式非常相似,除了用同步 WeakHashMap 代替 HashMap 來存儲 values 之外。(以一些額外的性能開銷為代價,使用 WeakHashMap 解決了無法對 Thread 對象進行垃圾回收的問題。)不用說, ThreadLocal 的性能是相當差的。

    Java 平臺版本 1.3 提供的 ThreadLocal 版本已經盡量更好了;它不使用任何同步,從而不存在可伸縮性問題,而且它也不使用弱引用。相反地,人們通過給 Thread 添加一個實例變量(該變量用于保存當前線程的從線程局部變量到它的值的映射的 HashMap )來修改 Thread 類以支持 ThreadLocal 。因為檢索或設置一個線程局部變量的過程不涉及對可能被另一個線程讀寫的數據的讀寫操作,所以您可以不用任何同步就實現 ThreadLocal.get()set() 。而且,因為每線程值的引用被存儲在自已的 Thread 對象中,所以當對 Thread 進行垃圾回收時,也能對該 Thread 的每線程值進行垃圾回收。

    不幸的是,即使有了這些改進,Java 1.3 中的 ThreadLocal 的性能仍然出奇地慢。據我的粗略測量,在雙處理器 Linux 系統上的 Sun 1.3 JDK 中進行 ThreadLocal.get() 操作,所耗費的時間大約是無爭用同步的兩倍。性能這么差的原因是 Thread.currentThread() 方法的花費非常大,占了 ThreadLocal.get() 運行時間的三分之二還多。雖然有這些缺點,JDK 1.3 ThreadLocal.get() 仍然比爭用同步快得多,所以如果在任何存在嚴重爭用的地方(可能是有非常多的線程,或者同步塊被頻繁地執行,或者同步塊很大), ThreadLocal 可能仍然要高效得多。

    在 Java 平臺的最新版本,即版本 1.4b2 中, ThreadLocalThread.currentThread() 的性能都有了很大提高。有了這些提高, ThreadLocal 應該比其它技術,如用池,更快。由于它比其它技術更簡單,也更不易出錯,人們最終將發現它是避免線程間出現不希望的交互的有效途徑。





    回頁首


    ThreadLocal 的好處

    ThreadLocal 能帶來很多好處。它常常是把有狀態類描繪成線程安全的,或者封裝非線程安全類以使它們能夠在多線程環境中安全地使用的最容易的方式。使用 ThreadLocal 使我們可以繞過為實現線程安全而對何時需要同步進行判斷的復雜過程,而且因為它不需要任何同步,所以也改善了可伸縮性。除簡單之外,用 ThreadLocal 存儲每線程單子或每線程上下文信息在歸檔方面還有一個頗有價值好處 — 通過使用 ThreadLocal ,存儲在 ThreadLocal 中的對象都是 被線程共享的是清晰的,從而簡化了判斷一個類是否線程安全的工作。

    posted @ 2006-08-24 17:32 Binary 閱讀(270) | 評論 (0)編輯 收藏

    Java 理論與實踐: 非阻塞算法簡介

    Java? 5.0 第一次讓使用 Java 語言開發非阻塞算法成為可能,java.util.concurrent 包充分地利用了這個功能。非阻塞算法屬于并發算法,它們可以安全地派生它們的線程,不通過鎖定派生,而是通過低級的原子性的硬件原生形式 —— 例如比較和交換。非阻塞算法的設計與實現極為困難,但是它們能夠提供更好的吞吐率,對生存問題(例如死鎖和優先級反轉)也能提供更好的防御。在這期的 Java 理論與實踐 中,并發性大師 Brian Goetz 演示了幾種比較簡單的非阻塞算法的工作方式。

    在不只一個線程訪問一個互斥的變量時,所有線程都必須使用同步,否則就可能會發生一些非常糟糕的事情。Java 語言中主要的同步手段就是 synchronized 關鍵字(也稱為內在鎖),它強制實行互斥,確保執行 synchronized 塊的線程的動作,能夠被后來執行受相同鎖保護的 synchronized 塊的其他線程看到。在使用得當的時候,內在鎖可以讓程序做到線程安全,但是在使用鎖定保護短的代碼路徑,而且線程頻繁地爭用鎖的時候,鎖定可能成為相當繁重的操作。

    “流行的原子” 一文中,我們研究了原子變量,原子變量提供了原子性的讀-寫-修改操作,可以在不使用鎖的情況下安全地更新共享變量。原子變量的內存語義與 volatile 變量類似,但是因為它們也可以被原子性地修改,所以可以把它們用作不使用鎖的并發算法的基礎。

    非阻塞的計數器

    清單 1 中的 Counter 是線程安全的,但是使用鎖的需求帶來的性能成本困擾了一些開發人員。但是鎖是必需的,因為雖然增加看起來是單一操作,但實際是三個獨立操作的簡化:檢索值,給值加 1,再寫回值。(在 getValue 方法上也需要同步,以保證調用 getValue 的線程看到的是最新的值。雖然許多開發人員勉強地使自己相信忽略鎖定需求是可以接受的,但忽略鎖定需求并不是好策略。)

    在多個線程同時請求同一個鎖時,會有一個線程獲勝并得到鎖,而其他線程被阻塞。JVM 實現阻塞的方式通常是掛起阻塞的線程,過一會兒再重新調度它。由此造成的上下文切換相對于鎖保護的少數幾條指令來說,會造成相當大的延遲。


    清單 1. 使用同步的線程安全的計數器
    												
    														public final class Counter {
        private long value = 0;
    
        public synchronized long getValue() {
            return value;
        }
    
        public synchronized long increment() {
            return ++value;
        }
    }
    
    												
    										

    清單 2 中的 NonblockingCounter 顯示了一種最簡單的非阻塞算法:使用 AtomicIntegercompareAndSet() (CAS)方法的計數器。compareAndSet() 方法規定 “將這個變量更新為新值,但是如果從我上次看到這個變量之后其他線程修改了它的值,那么更新就失敗”(請參閱 “流行的原子” 獲得關于原子變量以及 “比較和設置” 的更多解釋。)


    清單 2. 使用 CAS 的非阻塞算法
    												
    														public class NonblockingCounter {
        private AtomicInteger value;
    
        public int getValue() {
            return value.get();
        }
    
        public int increment() {
            int v;
            do {
                v = value.get();
            while (!value.compareAndSet(v, v + 1));
            return v + 1;
        }
    }
    
    												
    										

    原子變量類之所以被稱為原子的,是因為它們提供了對數字和對象引用的細粒度的原子更新,但是在作為非阻塞算法的基本構造塊的意義上,它們也是原子的。非阻塞算法作為科研的主題,已經有 20 多年了,但是直到 Java 5.0 出現,在 Java 語言中才成為可能。

    現代的處理器提供了特殊的指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,而 compareAndSet() 就用這些代替了鎖定。(如果要做的只是遞增計數器,那么 AtomicInteger 提供了進行遞增的方法,但是這些方法基于 compareAndSet(),例如 NonblockingCounter.increment())。

    非阻塞版本相對于基于鎖的版本有幾個性能優勢。首先,它用硬件的原生形態代替 JVM 的鎖定代碼路徑,從而在更細的粒度層次上(獨立的內存位置)進行同步,失敗的線程也可以立即重試,而不會被掛起后重新調度。更細的粒度降低了爭用的機會,不用重新調度就能重試的能力也降低了爭用的成本。即使有少量失敗的 CAS 操作,這種方法仍然會比由于鎖爭用造成的重新調度快得多。

    NonblockingCounter 這個示例可能簡單了些,但是它演示了所有非阻塞算法的一個基本特征 —— 有些算法步驟的執行是要冒險的,因為知道如果 CAS 不成功可能不得不重做。非阻塞算法通常叫作樂觀算法,因為它們繼續操作的假設是不會有干擾。如果發現干擾,就會回退并重試。在計數器的示例中,冒險的步驟是遞增 —— 它檢索舊值并在舊值上加一,希望在計算更新期間值不會變化。如果它的希望落空,就會再次檢索值,并重做遞增計算。





    回頁首


    非阻塞堆棧

    非阻塞算法稍微復雜一些的示例是清單 3 中的 ConcurrentStackConcurrentStack 中的 push()pop() 操作在結構上與 NonblockingCounter 上相似,只是做的工作有些冒險,希望在 “提交” 工作的時候,底層假設沒有失效。push() 方法觀察當前最頂的節點,構建一個新節點放在堆棧上,然后,如果最頂端的節點在初始觀察之后沒有變化,那么就安裝新節點。如果 CAS 失敗,意味著另一個線程已經修改了堆棧,那么過程就會重新開始。


    清單 3. 使用 Treiber 算法的非阻塞堆棧
    												
    														public class ConcurrentStack<E> {
        AtomicReference<Node<E>> head = new AtomicReference<Node<E>>();
    
        public void push(E item) {
            Node<E> newHead = new Node<E>(item);
            Node<E> oldHead;
            do {
                oldHead = head.get();
                newHead.next = oldHead;
            } while (!head.compareAndSet(oldHead, newHead));
        }
    
        public E pop() {
            Node<E> oldHead;
            Node<E> newHead;
            do {
                oldHead = head.get();
                if (oldHead == null) 
                    return null;
                newHead = oldHead.next;
            } while (!head.compareAndSet(oldHead,newHead));
            return oldHead.item;
        }
    
        static class Node<E> {
            final E item;
            Node<E> next;
    
            public Node(E item) { this.item = item; }
        }
    }
    
    												
    										

    性能考慮

    在輕度到中度的爭用情況下,非阻塞算法的性能會超越阻塞算法,因為 CAS 的多數時間都在第一次嘗試時就成功,而發生爭用時的開銷也不涉及線程掛起和上下文切換,只多了幾個循環迭代。沒有爭用的 CAS 要比沒有爭用的鎖便宜得多(這句話肯定是真的,因為沒有爭用的鎖涉及 CAS 加上額外的處理),而爭用的 CAS 比爭用的鎖獲取涉及更短的延遲。

    在高度爭用的情況下(即有多個線程不斷爭用一個內存位置的時候),基于鎖的算法開始提供比非阻塞算法更好的吞吐率,因為當線程阻塞時,它就會停止爭用,耐心地等候輪到自己,從而避免了進一步爭用。但是,這么高的爭用程度并不常見,因為多數時候,線程會把線程本地的計算與爭用共享數據的操作分開,從而給其他線程使用共享數據的機會。(這么高的爭用程度也表明需要重新檢查算法,朝著更少共享數據的方向努力。)“流行的原子” 中的圖在這方面就有點兒讓人困惑,因為被測量的程序中發生的爭用極其密集,看起來即使對數量很少的線程,鎖定也是更好的解決方案。





    回頁首


    非阻塞的鏈表

    目前為止的示例(計數器和堆棧)都是非常簡單的非阻塞算法,一旦掌握了在循環中使用 CAS,就可以容易地模仿它們。對于更復雜的數據結構,非阻塞算法要比這些簡單示例復雜得多,因為修改鏈表、樹或哈希表可能涉及對多個指針的更新。CAS 支持對單一指針的原子性條件更新,但是不支持兩個以上的指針。所以,要構建一個非阻塞的鏈表、樹或哈希表,需要找到一種方式,可以用 CAS 更新多個指針,同時不會讓數據結構處于不一致的狀態。

    在鏈表的尾部插入元素,通常涉及對兩個指針的更新:“尾” 指針總是指向列表中的最后一個元素,“下一個” 指針從過去的最后一個元素指向新插入的元素。因為需要更新兩個指針,所以需要兩個 CAS。在獨立的 CAS 中更新兩個指針帶來了兩個需要考慮的潛在問題:如果第一個 CAS 成功,而第二個 CAS 失敗,會發生什么?如果其他線程在第一個和第二個 CAS 之間企圖訪問鏈表,會發生什么?

    對于非復雜數據結構,構建非阻塞算法的 “技巧” 是確保數據結構總處于一致的狀態(甚至包括在線程開始修改數據結構和它完成修改之間),還要確保其他線程不僅能夠判斷出第一個線程已經完成了更新還是處在更新的中途,還能夠判斷出如果第一個線程走向 AWOL,完成更新還需要什么操作。如果線程發現了處在更新中途的數據結構,它就可以 “幫助” 正在執行更新的線程完成更新,然后再進行自己的操作。當第一個線程回來試圖完成自己的更新時,會發現不再需要了,返回即可,因為 CAS 會檢測到幫助線程的干預(在這種情況下,是建設性的干預)。

    這種 “幫助鄰居” 的要求,對于讓數據結構免受單個線程失敗的影響,是必需的。如果線程發現數據結構正處在被其他線程更新的中途,然后就等候其他線程完成更新,那么如果其他線程在操作中途失敗,這個線程就可能永遠等候下去。即使不出現故障,這種方式也會提供糟糕的性能,因為新到達的線程必須放棄處理器,導致上下文切換,或者等到自己的時間片過期(而這更糟)。

    清單 4 的 LinkedQueue 顯示了 Michael-Scott 非阻塞隊列算法的插入操作,它是由 ConcurrentLinkedQueue 實現的:


    清單 4. Michael-Scott 非阻塞隊列算法中的插入
    												
    														public class LinkedQueue <E> {
        private static class Node <E> {
            final E item;
            final AtomicReference<Node<E>> next;
    
            Node(E item, Node<E> next) {
                this.item = item;
                this.next = new AtomicReference<Node<E>>(next);
            }
        }
    
        private AtomicReference<Node<E>> head
            = new AtomicReference<Node<E>>(new Node<E>(null, null));
        private AtomicReference<Node<E>> tail = head;
    
        public boolean put(E item) {
            Node<E> newNode = new Node<E>(item, null);
            while (true) {
                Node<E> curTail = tail.get();
                Node<E> residue = curTail.next.get();
                if (curTail == tail.get()) {
                    if (residue == null) /* A */ {
                        if (curTail.next.compareAndSet(null, newNode)) /* C */ {
                            tail.compareAndSet(curTail, newNode) /* D */ ;
                            return true;
                        }
                    } else {
                        tail.compareAndSet(curTail, residue) /* B */;
                    }
                }
            }
        }
    }
    
    												
    										

    像許多隊列算法一樣,空隊列只包含一個假節點。頭指針總是指向假節點;尾指針總指向最后一個節點或倒數第二個節點。圖 1 演示了正常情況下有兩個元素的隊列:


    圖 1. 有兩個元素,處在靜止狀態的隊列

    清單 4 所示,插入一個元素涉及兩個指針更新,這兩個更新都是通過 CAS 進行的:從隊列當前的最后節點(C)鏈接到新節點,并把尾指針移動到新的最后一個節點(D)。如果第一步失敗,那么隊列的狀態不變,插入線程會繼續重試,直到成功。一旦操作成功,插入被當成生效,其他線程就可以看到修改。還需要把尾指針移動到新節點的位置上,但是這項工作可以看成是 “清理工作”,因為任何處在這種情況下的線程都可以判斷出是否需要這種清理,也知道如何進行清理。

    隊列總是處于兩種狀態之一:正常狀態(或稱靜止狀態,圖 1圖 3)或中間狀態(圖 2)。在插入操作之前和第二個 CAS(D)成功之后,隊列處在靜止狀態;在第一個 CAS(C)成功之后,隊列處在中間狀態。在靜止狀態時,尾指針指向的鏈接節點的 next 字段總為 null,而在中間狀態時,這個字段為非 null。任何線程通過比較 tail.next 是否為 null,就可以判斷出隊列的狀態,這是讓線程可以幫助其他線程 “完成” 操作的關鍵。


    圖 2. 處在插入中間狀態的隊列,在新元素插入之后,尾指針更新之前

    插入操作在插入新元素(A)之前,先檢查隊列是否處在中間狀態,如 清單 4 所示。如果是在中間狀態,那么肯定有其他線程已經處在元素插入的中途,在步驟(C)和(D)之間。不必等候其他線程完成,當前線程就可以 “幫助” 它完成操作,把尾指針向前移動(B)。如果有必要,它還會繼續檢查尾指針并向前移動指針,直到隊列處于靜止狀態,這時它就可以開始自己的插入了。

    第一個 CAS(C)可能因為兩個線程競爭訪問隊列當前的最后一個元素而失??;在這種情況下,沒有發生修改,失去 CAS 的線程會重新裝入尾指針并再次嘗試。如果第二個 CAS(D)失敗,插入線程不需要重試 —— 因為其他線程已經在步驟(B)中替它完成了這個操作!


    圖 3. 在尾指針更新后,隊列重新處在靜止狀態

    幕后的非阻塞算法

    如果深入 JVM 和操作系統,會發現非阻塞算法無處不在。垃圾收集器使用非阻塞算法加快并發和平行的垃圾搜集;調度器使用非阻塞算法有效地調度線程和進程,實現內在鎖。在 Mustang(Java 6.0)中,基于鎖的 SynchronousQueue 算法被新的非阻塞版本代替。很少有開發人員會直接使用 SynchronousQueue,但是通過 Executors.newCachedThreadPool() 工廠構建的線程池用它作為工作隊列。比較緩存線程池性能的對比測試顯示,新的非阻塞同步隊列實現提供了幾乎是當前實現 3 倍的速度。在 Mustang 的后續版本(代碼名稱為 Dolphin)中,已經規劃了進一步的改進。





    回頁首


    結束語

    非阻塞算法要比基于鎖的算法復雜得多。開發非阻塞算法是相當專業的訓練,而且要證明算法的正確也極為困難。但是在 Java 版本之間并發性能上的眾多改進來自對非阻塞算法的采用,而且隨著并發性能變得越來越重要,可以預見在 Java 平臺的未來發行版中,會使用更多的非阻塞算法。

    posted @ 2006-08-24 17:31 Binary 閱讀(312) | 評論 (0)編輯 收藏

    Java 理論和實踐: 了解泛型

    JDK 5.0 中增加的泛型類型,是 Java 語言中類型安全的一次重要改進。但是,對于初次使用泛型類型的用戶來說,泛型的某些方面看起來可能不容易明白,甚至非常奇怪。在本月的“Java 理論和實踐”中,Brian Goetz 分析了束縛第一次使用泛型的用戶的常見陷阱。您可以通過討論論壇與作者和其他讀者分享您對本文的看法。(也可以單擊本文頂端或底端的討論來訪問這個論壇。)

    表面上看起來,無論語法還是應用的環境(比如容器類),泛型類型(或者泛型)都類似于 C++ 中的模板。但是這種相似性僅限于表面,Java 語言中的泛型基本上完全在編譯器中實現,由編譯器執行類型檢查和類型推斷,然后生成普通的非泛型的字節碼。這種實現技術稱為擦除(erasure)(編譯器使用泛型類型信息保證類型安全,然后在生成字節碼之前將其清除),這項技術有一些奇怪,并且有時會帶來一些令人迷惑的后果。雖然范型是 Java 類走向類型安全的一大步,但是在學習使用泛型的過程中幾乎肯定會遇到頭痛(有時候讓人無法忍受)的問題。

    注意:本文假設您對 JDK 5.0 中的范型有基本的了解。

    泛型不是協變的

    雖然將集合看作是數組的抽象會有所幫助,但是數組還有一些集合不具備的特殊性質。Java 語言中的數組是協變的(covariant),也就是說,如果 Integer 擴展了 Number(事實也是如此),那么不僅 IntegerNumber,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以傳遞或者賦予 Integer[]。(更正式地說,如果 NumberInteger 的超類型,那么 Number[] 也是 Integer[] 的超類型)。您也許認為這一原理同樣適用于泛型類型 —— List<Number>List<Integer> 的超類型,那么可以在需要 List<Number> 的地方傳遞 List<Integer>。不幸的是,情況并非如此。

    不允許這樣做有一個很充分的理由:這樣做將破壞要提供的類型安全泛型。如果能夠將 List<Integer> 賦給 List<Number>。那么下面的代碼就允許將非 Integer 的內容放入 List<Integer>

    												
    														List<Integer> li = new ArrayList<Integer>();
    List<Number> ln = li; // illegal
    ln.add(new Float(3.1415));
    
    												
    										

    因為 lnList<Number>,所以向其添加 Float 似乎是完全合法的。但是如果 lnli 的別名,那么這就破壞了蘊含在 li 定義中的類型安全承諾 —— 它是一個整數列表,這就是泛型類型不能協變的原因。

    其他的協變問題

    數組能夠協變而泛型不能協變的另一個后果是,不能實例化泛型類型的數組(new List<String>[3] 是不合法的),除非類型參數是一個未綁定的通配符(new List<?>[3] 是合法的)。讓我們看看如果允許聲明泛型類型數組會造成什么后果:

    												
    														List<String>[] lsa = new List<String>[10]; // illegal
    Object[] oa = lsa;  // OK because List<String> is a subtype of Object
    List<Integer> li = new ArrayList<Integer>();
    li.add(new Integer(3));
    oa[0] = li; 
    String s = lsa[0].get(0); 
    
    												
    										

    最后一行將拋出 ClassCastException,因為這樣將把 List<Integer> 填入本應是 List<String> 的位置。因為數組協變會破壞泛型的類型安全,所以不允許實例化泛型類型的數組(除非類型參數是未綁定的通配符,比如 List<?>)。





    回頁首


    構造延遲

    因為可以擦除功能,所以 List<Integer>List<String> 是同一個類,編譯器在編譯 List<V> 時只生成一個類(和 C++ 不同)。因此,在編譯 List<V> 類時,編譯器不知道 V 所表示的類型,所以它就不能像知道類所表示的具體類型那樣處理 List<V> 類定義中的類型參數(List<V> 中的 V)。

    因為運行時不能區分 List<String>List<Integer>(運行時都是 List),用泛型類型參數標識類型的變量的構造就成了問題。運行時缺乏類型信息,這給泛型容器類和希望創建保護性副本的泛型類提出了難題。

    比如泛型類 Foo

    												
    														class Foo<T> { 
      public void doSomething(T param) { ... }
    }
    
    												
    										

    假設 doSomething() 方法希望復制輸入的 param 參數,會怎么樣呢?沒有多少選擇。您可能希望按以下方式實現 doSomething()

    												
    														public void doSomething(T param) { 
      T copy = new T(param);  // illegal
    }
    
    												
    										

    但是您不能使用類型參數訪問構造函數,因為在編譯的時候還不知道要構造什么類,因此也就不知道使用什么構造函數。使用泛型不能表達“T 必須擁有一個拷貝構造函數(copy constructor)”(甚至一個無參數的構造函數)這類約束,因此不能使用泛型類型參數所表示的類的構造函數。

    clone() 怎么樣呢?假設在 Foo 的定義中,T 擴展了 Cloneable

    												
    														class Foo<T extends Cloneable> { 
      public void doSomething(T param) {
        T copy = (T) param.clone();  // illegal 
      }
    }
    
    												
    										

    不幸的是,仍然不能調用 param.clone()。為什么呢?因為 clone()Object 中是保護訪問的,調用 clone() 必須通過將 clone() 改寫公共訪問的類引用來完成。但是重新聲明 clone() 為 public 并不知道 T,因此克隆也無濟于事。

    構造通配符引用

    因此,不能復制在編譯時根本不知道是什么類的類型引用。那么使用通配符類型怎么樣?假設要創建類型為 Set<?> 的參數的保護性副本。您知道 Set 有一個拷貝構造函數。而且別人可能曾經告訴過您,如果不知道要設置的內容的類型,最好使用 Set<?> 代替原始類型的 Set,因為這種方法引起的未檢查類型轉換警告更少。于是,可以試著這樣寫:

    												
    														class Foo {
      public void doSomething(Set<?> set) {
        Set<?> copy = new HashSet<?>(set);  // illegal
      }
    }
    
    												
    										

    不幸的是,您不能用通配符類型的參數調用泛型構造函數,即使知道存在這樣的構造函數也不行。不過您可以這樣做:

    												
    														class Foo {
      public void doSomething(Set<?> set) {
        Set<?> copy = new HashSet<Object>(set);  
      }
    }
    
    												
    										

    這種構造不那么直觀,但它是類型安全的,而且可以像 new HashSet<?>(set) 那樣工作。

    構造數組

    如何實現 ArrayList<V>?假設類 ArrayList 管理一個 V 數組,您可能希望用 ArrayList<V> 的構造函數創建一個 V 數組:

    												
    														class ArrayList<V> {
      private V[] backingArray;
      public ArrayList() {
        backingArray = new V[DEFAULT_SIZE]; // illegal
      }
    }
    
    												
    										

    但是這段代碼不能工作 —— 不能實例化用類型參數表示的類型數組。編譯器不知道 V 到底表示什么類型,因此不能實例化 V 數組。

    Collections 類通過一種別扭的方法繞過了這個問題,在 Collections 類編譯時會產生類型未檢查轉換的警告。ArrayList 具體實現的構造函數如下:

    												
    														class ArrayList<V> {
      private V[] backingArray;
      public ArrayList() {
        backingArray = (V[]) new Object[DEFAULT_SIZE]; 
      }
    }
    
    												
    										

    為何這些代碼在訪問 backingArray 時沒有產生 ArrayStoreException 呢?無論如何,都不能將 Object 數組賦給 String 數組。因為泛型是通過擦除實現的,backingArray 的類型實際上就是 Object[],因為 Object 代替了 V。這意味著:實際上這個類期望 backingArray 是一個 Object 數組,但是編譯器要進行額外的類型檢查,以確保它包含 V 類型的對象。所以這種方法很奏效,但是非常別扭,因此不值得效仿(甚至連泛型 Collections 框架的作者都這么說,請參閱參考資料)。

    還有一種方法就是聲明 backingArrayObject 數組,并在使用它的各個地方強制將它轉化為 V[]。仍然會看到類型未檢查轉換警告(與上一種方法一樣),但是它使一些未明確的假設更清楚了(比如 backingArray 不應逃避 ArrayList 的實現)。

    其他方法

    最好的辦法是向構造函數傳遞類文字(Foo.class),這樣,該實現就能在運行時知道 T 的值。不采用這種方法的原因在于向后兼容性 —— 新的泛型集合類不能與 Collections 框架以前的版本兼容。

    下面的代碼中 ArrayList 采用了以下方法:

    												
    														public class ArrayList<V> implements List<V> {
      private V[] backingArray;
      private Class<V> elementType;
    
      public ArrayList(Class<V> elementType) {
        this.elementType = elementType;
        backingArray = (V[]) Array.newInstance(elementType, DEFAULT_LENGTH);
      }
    }
    
    												
    										

    但是等一等!仍然有不妥的地方,調用 Array.newInstance() 時會引起未經檢查的類型轉換。為什么呢?同樣是由于向后兼容性。Array.newInstance() 的簽名是:

    												
    														public static Object newInstance(Class<?> componentType, int length)
    
    												
    										

    而不是類型安全的:

    												
    														public static<T> T[] newInstance(Class<T> componentType, int length)
    
    												
    										

    為何 Array 用這種方式進行泛化呢?同樣是為了保持向后兼容。要創建基本類型的數組,如 int[],可以使用適當的包裝器類中的 TYPE 字段調用 Array.newInstance()(對于 int,可以傳遞 Integer.TYPE 作為類文字)。用 Class<T> 參數而不是 Class<?> 泛化 Array.newInstance(),對于引用類型有更好的類型安全,但是就不能使用 Array.newInstance() 創建基本類型數組的實例了。也許將來會為引用類型提供新的 newInstance() 版本,這樣就兩者兼顧了。

    在這里可以看到一種模式 —— 與泛型有關的很多問題或者折衷并非來自泛型本身,而是保持和已有代碼兼容的要求帶來的副作用。





    回頁首


    泛化已有的類

    在轉化現有的庫類來使用泛型方面沒有多少技巧,但與平常的情況相同,向后兼容性不會憑空而來。我已經討論了兩個例子,其中向后兼容性限制了類庫的泛化。

    另一種不同的泛化方法可能不存在向后兼容問題,這就是 Collections.toArray(Object[])。傳入 toArray() 的數組有兩個目的 —— 如果集合足夠小,那么可以將其內容直接放在提供的數組中。否則,利用反射(reflection)創建相同類型的新數組來接受結果。如果從頭開始重寫 Collections 框架,那么很可能傳遞給 Collections.toArray() 的參數不是一個數組,而是一個類文字:

    												
    														interface Collection<E> { 
      public T[] toArray(Class<T super E> elementClass);
    }
    
    												
    										

    因為 Collections 框架作為良好類設計的例子被廣泛效仿,但是它的設計受到向后兼容性約束,所以這些地方值得您注意,不要盲目效仿。

    首先,常常被混淆的泛型 Collections API 的一個重要方面是 containsAll()、removeAll()retainAll() 的簽名。您可能認為 remove()removeAll() 的簽名應該是:

    												
    														interface Collection<E> { 
      public boolean remove(E e);  // not really
      public void removeAll(Collection<? extends E> c);  // not really
    }
    
    												
    										

    但實際上卻是:

    												
    														interface Collection<E> { 
      public boolean remove(Object o);  
      public void removeAll(Collection<?> c);
    }
    
    												
    										

    為什么呢?答案同樣是因為向后兼容性。x.remove(o) 的接口表明“如果 o 包含在 x 中,則刪除它,否則什么也不做。”如果 x 是一個泛型集合,那么 o 不一定與 x 的類型參數兼容。如果 removeAll() 被泛化為只有類型兼容時才能調用(Collection<? extends E>),那么在泛化之前,合法的代碼序列就會變得不合法,比如:

    												
    														// a collection of Integers
    Collection c = new HashSet();
    // a collection of Objects
    Collection r = new HashSet();
    c.removeAll(r);
    
    												
    										

    如果上述片段用直觀的方法泛化(將 c 設為 Collection<Integer>,r 設為 Collection<Object>),如果 removeAll() 的簽名要求其參數為 Collection<? extends E> 而不是 no-op,那么就無法編譯上面的代碼。泛型類庫的一個主要目標就是不打破或者改變已有代碼的語義,因此,必須用比從頭重新設計泛型所使用類型約束更弱的類型約束來定義 remove()、removeAll()、retainAll()containsAll()

    在泛型之前設計的類可能阻礙了“顯然的”泛型化方法。這種情況下就要像上例這樣進行折衷,但是如果從頭設計新的泛型類,理解 Java 類庫中的哪些東西是向后兼容的結果很有意義,這樣可以避免不適當的模仿。





    回頁首


    擦除的實現

    因為泛型基本上都是在 Java 編譯器中而不是運行庫中實現的,所以在生成字節碼的時候,差不多所有關于泛型類型的類型信息都被“擦掉”了。換句話說,編譯器生成的代碼與您手工編寫的不用泛型、檢查程序的類型安全后進行強制類型轉換所得到的代碼基本相同。與 C++ 不同,List<Integer>List<String> 是同一個類(雖然是不同的類型但都是 List<?> 的子類型,與以前的版本相比,在 JDK 5.0 中這是一個更重要的區別)。

    擦除意味著一個類不能同時實現 Comparable<String>Comparable<Number>,因為事實上兩者都在同一個接口中,指定同一個 compareTo() 方法。聲明 DecimalString 類以便與 StringNumber 比較似乎是明智的,但對于 Java 編譯器來說,這相當于對同一個方法進行了兩次聲明:

    												
    														public class DecimalString implements Comparable<Number>, Comparable<String> { ... } // nope
    
    												
    										

    擦除的另一個后果是,對泛型類型參數是用強制類型轉換或者 instanceof 毫無意義。下面的代碼完全不會改善代碼的類型安全性:

    												
    														public <T> T naiveCast(T t, Object o) { return (T) o; }
    
    												
    										

    編譯器僅僅發出一個類型未檢查轉換警告,因為它不知道這種轉換是否安全。naiveCast() 方法實際上根本不作任何轉換,T 直接被替換為 Object,與期望的相反,傳入的對象被強制轉換為 Object

    擦除也是造成上述構造問題的原因,即不能創建泛型類型的對象,因為編譯器不知道要調用什么構造函數。如果泛型類需要構造用泛型類型參數來指定類型的對象,那么構造函數應該接受類文字(Foo.class)并將它們保存起來,以便通過反射創建實例。

    posted @ 2006-08-24 17:31 Binary 閱讀(265) | 評論 (0)編輯 收藏

    一個線程池的實現

    設計目標
    ?????提供一個線程池的組件,具有良好的伸縮性,當線程夠用時,銷毀不用線程,當線程不夠用時,自動增加線程數量;
    ?????提供一個工作任務接口和工作隊列,實際所需要的任務都必須實現這個工作任務接口,然后放入工作隊列中;
    ?????線程池中的線程從工作隊列中,自動取得工作任務,執行任務。
    主要控制類和功能接口設計
    線程池管理器?ThreadPoolManager?的功能:
    ?????管理線程池中的各個屬性變量
    ü????最大工作線程數
    ü????最小工作線程數
    ü????激活的工作線程總數
    ü????睡眠的工作線程總數
    ü????工作線程總數?(即:激活的工作線程總數+睡眠的工作線程總數)
    ?????創建工作線程
    ?????銷毀工作線程
    ?????啟動處于睡眠的工作線程
    ?????睡眠處于激活的工作線程
    ?????縮任務:當工作線程總數小于或等于最小工作線程數時,銷毀多余的睡眠的工作線程,使得現有工作線程總數等于最小工作任務總數
    ?????伸任務:當任務隊列任務總數大于工作線程數時,增加工作線程總數至最大工作線程數
    ?????提供線程池啟動接口
    ?????提供線程池銷毀接口
    工作線程?WorkThread??的功能:
    ?????從工作隊列取得工作任務
    ?????執行工作任務接口中的指定任務
    工作任務接口?ITask???的功能:
    ?????提供指定任務動作
    工作隊列?IWorkQueue??的功能:
    ?????提供獲取任務接口,并刪除工作隊列中的任務;
    ?????提供加入任務接口;
    ?????提供刪除任務接口;
    ?????提供取得任務總數接口;
    ?????提供自動填任務接口;(當任務總數少于或等于默認總數的25%時,自動裝填)
    ?????提供刪除所有任務接口;


    Code


    ThreadPoolManager:
    =====================================
    CODE:
    package test.thread.pool1;
    import java.util.ArrayList;
    import java.util.List;
    import test.thread.pool1.impl.MyWorkQueue;
    
    /**
     * <p>Title: 線程池管理器</p>
     * <p>Description: </p>
     * <p>Copyright: Copyright (c) 2005</p>
     * <p>Company: </p>
     * @author not attributable
     * @version 1.0
     */
    
    public class ThreadPoolManager {
      /*最大線程數*/
      private int threads_max_num;
    
      /*最小線程數*/
      private int threads_min_num;
      
      /* 線程池線程增長步長 */
      private int threads_increase_step = 5;
    
      /* 任務工作隊列 */
      private IWorkQueue queue;
      
      /* 線程池監視狗 */
      private PoolWatchDog poolWatchDog ;
      
      /* 隊列線程 */
      private Thread queueThread ;
      
      /* 線程池 封裝所有工作線程的數據結構 */
      private List pool = new ArrayList();
      
      /* 線程池中 封裝所有鈍化后的數據結構*/
      private List passivePool = new ArrayList();
      
      /* 空閑60秒 */
      private static final long IDLE_TIMEOUT = 60000L;
      
      /* 關閉連接池標志位 */
      private boolean close = false;
      
      /**
       * 線程池管理器
       * @param queue 任務隊列
       * @param threads_min_num 工作線程最小數
       * @param threads_max_num 工作線程最大數
       */
      public ThreadPoolManager(int threads_max_num
                              ,int threads_min_num
                              ,IWorkQueue queue){
        this.threads_max_num = threads_max_num;
        this.threads_min_num = threads_min_num;
        this.queue = queue;    
      }
    
      /**
       * 線程池啟動
       */
      public void startPool(){
        System.out.println("=== startPool..........");
        poolWatchDog = new PoolWatchDog("PoolWatchDog");
        poolWatchDog.setDaemon(true);
        poolWatchDog.start();
        System.out.println("=== startPool..........over");
      }
    
      /**
       * 線程池銷毀接口
       */
      public void destoryPool(){
        System.out.println("==========================DestoryPool starting ...");
        this.close = true;
        int pool_size = this.pool.size();
        
        //中斷隊列線程
        System.out.println("===Interrupt queue thread ... ");
        queueThread.interrupt();
        queueThread = null;
        
        System.out.println("===Interrupt thread pool ... ");
        Thread pool_thread = null;
        for(int i=0; i<pool_size; i++){
          pool_thread = (Thread)pool.get(i);
          if(pool_thread !=null 
          && pool_thread.isAlive() 
          && !pool_thread.isInterrupted()){
            pool_thread.interrupt();
            System.out.println("Stop pool_thread:"
                              +pool_thread.getName()+"[interrupt] "
                              +pool_thread.isInterrupted());
          }
        }//end for
        
        if(pool != null){
          pool.clear();
        }
        if(passivePool != null){
          pool.clear();
        }
        
        try{
          System.out.println("=== poolWatchDog.join() starting ...");
          poolWatchDog.join();
          System.out.println("=== poolWatchDog.join() is over ...");
        }
        catch(Throwable ex){
          System.out.println("###poolWatchDog ... join method throw a exception ... "
                              +ex.toString());
        }
        
        poolWatchDog =null;
        System.out.println("==============================DestoryPool is over ...");    
      }
      
      
      public static void main(String[] args) throws Exception{
        ThreadPoolManager threadPoolManager1 = new ThreadPoolManager(10,5,new MyWorkQueue(50,30000));
        
        threadPoolManager1.startPool();
        Thread.sleep(60000);
        threadPoolManager1.destoryPool();
      }
      
      /**
       * 線程池監視狗
       */
      private class PoolWatchDog extends Thread{
        public PoolWatchDog(String name){
          super(name);
        }
      
        public void run(){
          Thread workThread = null;
          Runnable run = null;
          
          //開啟任務隊列線程,獲取數據--------
          System.out.println("===QueueThread starting ... ... ");
          queueThread = new Thread(new QueueThread(),"QueueThread");
          queueThread.start();
          
          System.out.println("===Initial thread Pool ... ...");
          //初始化線程池的最小線程數,并放入池中
          for(int i=0; i<threads_min_num; i++){
            run = new WorkThread();
            workThread = new Thread(run,"WorkThread_"+System.currentTimeMillis()+i);
            workThread.start();
            if(i == threads_min_num -1){
              workThread = null;
              run = null;
            }
          }
          System.out.println("===Initial thread Pool..........over ,and get pool's size:"+pool.size());
    
          //線程池線程動態增加線程算法--------------
          while(!close){
          
            //等待5秒鐘,等上述線程都啟動----------
            synchronized(this){          
              try{
                System.out.println("===Wait the [last time] threads starting ....");
                this.wait(15000);
              }
              catch(Throwable ex){
                System.out.println("###PoolWatchDog invoking is failure ... "+ex);
              }
            }//end synchronized
              
            //開始增加線程-----------------------spread動作
            int queue_size = queue.getTaskSize();
            int temp_size = (queue_size - threads_min_num);
            
            if((temp_size > 0) && (temp_size/threads_increase_step > 2) ){
              System.out.println("================Spread thread pool starting ....");
              for(int i=0; i<threads_increase_step && (pool.size() < threads_max_num); i++){
                System.out.println("=== Spread thread num : "+i);
                run = new WorkThread();
                workThread = new Thread(run,"WorkThread_"+System.currentTimeMillis()+i);
                workThread.start();
              }//end for
              
              workThread = null;
              run = null;    
              System.out.println("===Spread thread pool is over .... and pool size:"+pool.size());
            }//end if
              
            //刪除已經多余的睡眠線程-------------shrink動作
            int more_sleep_size = pool.size() - threads_min_num;//最多能刪除的線程數
            int sleep_threads_size = passivePool.size();
            if(more_sleep_size >0 && sleep_threads_size >0){
              System.out.println("================Shrink thread pool starting ....");        
              for(int i=0; i < more_sleep_size && i < sleep_threads_size ; i++){
                System.out.println("=== Shrink thread num : "+i);
                Thread removeThread = (Thread)passivePool.get(0);
                if(removeThread != null && removeThread.isAlive() && !removeThread.isInterrupted()){
                  removeThread.interrupt();
                }
              }
              System.out.println("===Shrink thread pool is over .... and pool size:"+pool.size());          
            }
    
            System.out.println("===End one return [shrink - spread operator] ....");    
          }//end while
        }//end run 
      }//end private class
      
      /**
       * 工作線程
       */
      class WorkThread implements Runnable{
      
        public WorkThread(){
        }
      
        public void run(){
          String name = Thread.currentThread().getName();
          System.out.println("===Thread.currentThread():"+name);
          pool.add(Thread.currentThread());    
        
          while(true){
          
            //獲取任務---------
            ITask task = null;
            try{
              System.out.println("===Get task from queue is starting ... ");
              //看線程是否被中斷,如果被中斷停止執行任務----
              if(Thread.currentThread().isInterrupted()){
                System.out.println("===Breaking current thread and jump whlie [1] ... ");
                break;
              }
              task = queue.getTask();
            }
            catch(Throwable ex){
              System.out.println("###No task in queue:"+ex);
            }//end tryc
            
            if(task != null){
              //執行任務---------
              try{
                System.out.println("===Execute the task is starting ... ");
                //看線程是否被中斷,如果被中斷停止執行任務----
                if(Thread.currentThread().isInterrupted()){
                  System.out.println("===Breaking current thread and jump whlie [1] ... ");
                  break;
                }     
                task.executeTask();
                //任務執行完畢-------
                System.out.println("===Execute the task is over ... ");
              }
              catch(Throwable ex){
                System.out.println("###Execute the task is failure ... "+ex);
              }//end tryc
              
            }else{
              //沒有任務,則鈍化線程至規定時間--------
              synchronized(this){
                try{
                  System.out.println("===Passivate into passivePool ... ");
                  
                  //看線程是否被中斷,如果被中斷停止執行任務----
                  boolean isInterrupted = Thread.currentThread().isInterrupted();
                  if(isInterrupted){
                    System.out.println("===Breaking current thread and jump whlie [1] ... ");
                    break;
                  }
    //              passivePool.add(this);
                passivePool.add(Thread.currentThread());
    
                  
                  //準備睡眠線程-------
                  isInterrupted = Thread.currentThread().isInterrupted();
                  if(isInterrupted){
                    System.out.println("===Breaking current thread and jump whlie [2] ... ");
                    break;
                  }              
                  this.wait(IDLE_TIMEOUT);
                }
                catch(Throwable ex1){
                  System.out.println("###Current Thread passivate is failure ... break while cycle. "+ex1);
                  break;
                }
              }          
            }        
          }//end while--------
          
          if(pool.contains(passivePool)){
            pool.remove(this);
          }
          if(passivePool.contains(passivePool)){
            passivePool.remove(this);
          }
          System.out.println("===The thread execute over ... "); 
        }//end run----------
      }
      
      
      class QueueThread implements Runnable{
      
        public QueueThread(){
        }
      
        public void run(){
          while(true){
            //自動裝在任務--------
            queue.autoAddTask();
            System.out.println("===The size of queue's task is "+queue.getTaskSize());
          
            synchronized(this){
              if(Thread.currentThread().isInterrupted()){
                break;
              }else{
                  try{
                    this.wait(queue.getLoadDataPollingTime());
                  }
                  catch(Throwable ex){
                    System.out.println("===QueueThread invoked wait is failure ... break while cycle."+ex);
                    break;
                  }
              }//end if
            }//end synchr
            
          }//end while
        }//end run
      } 
    }
    






    WorkQueue
    =====================================
    CODE:
    package test.thread.pool1;
    
    import java.util.LinkedList;
    import test.thread.pool1.impl.MyTask;
    
    /**
     * <p>Title: 工作隊列對象 </p>
     * <p>Description: </p>
     * <p>Copyright: Copyright (c) 2005</p>
     * <p>Company: </p>
     * @author not attributable
     * @version 1.0
     */
    
    public abstract class WorkQueue implements IWorkQueue{
      /* 預計裝載量 */
      private int load_size;
      
      /* 數據裝載輪循時間 */
      private long load_polling_time;
      
      /* 隊列 */
      private LinkedList queue = new LinkedList();
      
      /**
       * 
       * @param load_size 預計裝載量
       * @param load_polling_time 數據裝載輪循時間
       */
      public WorkQueue(int load_size,long load_polling_time){
        this.load_size = (load_size <= 10) ? 10 : load_size;
        this.load_polling_time = load_polling_time;
      }
    
      /* 數據裝載輪循時間 */
      public long getLoadDataPollingTime(){
        return this.load_polling_time;
      }
    
    
      /*獲取任務,并刪除隊列中的任務*/
      public synchronized ITask getTask(){
        ITask task = (ITask)queue.getFirst();
        queue.removeFirst();
        return task;
      }
    
      /*加入任務*/
      public void  addTask(ITask task){
        queue.addLast(task);
      }
    
      /*刪除任務*/
      public synchronized void removeTask(ITask task){
        queue.remove(task);
      }
    
      /*任務總數*/
      public synchronized int getTaskSize(){
        return queue.size();
      }
    
      /*自動裝填任務*/
      public synchronized void autoAddTask(){
      
        synchronized(this){
          float load_size_auto = load_size - getTaskSize() / load_size;
          System.out.println("===load_size_auto:"+load_size_auto);
          
          if(load_size_auto > 0.25){        
            autoAddTask0();
          }
          else {
            System.out.println("=== Not must load new work queue ... Now! ");
          }    
        }
      }
    
      /*刪除所有任務*/
      public synchronized void clearAllTask(){
        queue.clear();
      }
      
      /**
       * 程序員自己實現該方法
       */
      protected abstract void autoAddTask0();
    }
    





    MyWorkQueue
    =====================================
    CODE:
    package test.thread.pool1.impl;
    
    import java.util.LinkedList;
    import test.thread.pool1.WorkQueue;
    
    /**
     * <p>Title: 例子工作隊列對象 </p>
     * <p>Description: </p>
     * <p>Copyright: Copyright (c) 2005</p>
     * <p>Company: </p>
     * @author not attributable
     * @version 1.0
     */
    
    public class MyWorkQueue extends WorkQueue{
    
      /**
       * @param load_size 預計裝載量
       * @param load_polling_time 數據裝載輪循時間
       */
      public MyWorkQueue(int load_size,long load_polling_time){
        super(load_size,load_polling_time);
      }
    
      /**
       * 自動加載任務
       */
      protected synchronized void autoAddTask0(){
        //-------------------
        System.out.println("===MyWorkQueue ...  invoked autoAddTask0() method ...");
        for(int i=0; i<10; i++){
          System.out.println("===add task :"+i);
          this.addTask(new MyTask());
        }    
        //-------------------
      }
    }
    





    MyTask
    =====================================
    CODE:
    package test.thread.pool1.impl;
    import test.thread.pool1.ITask;
    
    /**
     * <p>Title: 工作任務接口 </p>
     * <p>Description: </p>
     * <p>Copyright: Copyright (c) 2005</p>
     * <p>Company: </p>
     * @author not attributable
     * @version 1.0
     */
    
    public class MyTask implements ITask {
    
      /**
       * 執行的任務
       * @throws java.lang.Throwable
       */
      public void executeTask() throws Throwable{
        System.out.println("["+this.hashCode()+"] MyTask ... invoked executeTask() method ... ");
      }
    }
    

    posted @ 2006-08-24 16:55 Binary 閱讀(3732) | 評論 (2)編輯 收藏

    正確優雅的解決用戶退出問題——JSP和Struts解決方案

    注:本文是由 馬嘉楠 翻譯的javaworld.com上的一篇名為《Solving the logout problem properly and elegantly》的文章,原文請參看 Solving the logout problem properly and elegantly 。
    花了2天翻譯過后才發現wolfmanchen已經捷足先登了,而且翻譯得很準確(比我的好,^+^)我進行了修改,在此謝謝wolfmanchen。
    文中所有示例程序的代碼可以從javaworld.com中下載,文章后面有資源鏈接。

    我看過之后覺得很好,希望對你也有所幫助!


    ?????????????????????????????正確優雅的解決用戶退出問題
    ?????????????????????????????????????????? ------JSP和Struts解決方案


    摘要

    在一個有密碼保護的Web應用當中,正確妥善的處理用戶退出過程并不僅僅只需要調用HttpSession對象的invalidate()方法,因為現在大部分瀏覽器上都有后退(Back)和前進(Forward)按鈕,允許用戶后退或前進到一個頁面。在用戶退出一個Web應用之后,如果按了后退按鈕,瀏覽器把緩存中的頁面呈現給用戶,這會使用戶產生疑惑,他們會開始擔心他們的個人數據是否安全。

    實際上,許多Web應用會彈出一個頁面,警告用戶退出時關閉整個瀏覽器,以此來阻止用戶點擊后退按鈕。還有一些使用JavaScript,但在某些客戶端瀏覽器中這卻不一定起作用。這些解決方案大多數實現都很笨拙,且不能保證在任何情況下都100%有效,同時,它還要求用戶有一定的操作經驗。

    這篇文章以簡單的程序示例闡述了正確解決用戶退出問題的方案。作者Kevin Le首先描述了一個理想的密碼保護Web應用,然后以示例程序解釋問題如何產生并討論解決問題的方案。文章雖然是針對JSP進行討論闡述,但作者所闡述的概念很容易理解而且能夠為其他Web技術所采用。最后最后,作者Kevin Le用Jakarta Struts更為優雅地解決用戶退出問題。文中包含JSP和Struts的示例程序 (3,700 words; September 27, 2004)




    大部分Web應用不會包含像銀行賬戶或信用卡資料那樣機密的信息,但是一旦涉及到敏感數據,就需要我們提供某些密碼保護機制。例如,在一個工廠當中,工人必須通過Web應用程序訪問他們的時間安排、進入他們的培訓課程以及查看他們的薪金等等。此時應用SSL(Secure Socket Layer)就有些大材小用了(SSL頁面不會在緩存中保存,關于SSL的討論已經超出本文的范圍)。但是這些應用又確實需要某種密碼保護措施,否則,工人(在這種情況下,也就是Web應用的使用者)就可以發現工廠中所有員工的私人機密信息。

    類似上面的情況還包括位于公共圖書館、醫院、網吧等公共場所的計算機。在這些地方,許多用戶共同使用幾臺計算機,此時保護用戶的個人數據就顯得至關重要。
    同時應用程序的良好設計與實現對用戶專業知識以及相關培訓要求少之又少。

    讓我們來看一下現實世界中一個完美的Web應用是怎樣工作的:
    1. 用戶在瀏覽器中輸入URL,訪問一個頁面。
    2. Web應用顯示一個登陸頁面,要求用戶輸入有效的驗證信息。
    3. 用戶輸入用戶名和密碼。
    4. 假設用戶提供的驗證信息是正確的,經過了驗證過程,Web應用允許用戶瀏覽他有權訪問的區域。
    5. 退出時,用戶點擊頁面的退出按鈕,Web應用顯示確認頁面,詢問用戶是否真的需要退出。一旦用戶點擊確定按鈕,Session結束,Web應用重新定位到登陸頁面。用戶現在可以放心的離開而不用擔心他的信息會被泄露。
    6. 另一個用戶坐到了同一臺電腦前。他點擊后退按鈕,Web應用不應該顯示上一個用戶訪問過的任何一個頁面。
    事實上,Web應用將一直停留在登陸頁面上,除非第二個用戶提供正確的驗證信息,之后才可以訪問他有權限的區域。

    通過示例程序,文章向您闡述了如何在一個Web應用中實現上面的功能。




    一. JSP samples

    為了更為有效地向您說明這個解決方案,本文將從展示一個Web應用logoutSampleJSP1中碰到的問題開始。這個示例代表了許多沒有正確解決退出過程的Web應用。logoutSampleJSP1包含一下JSP頁面:login.jsp,? home.jsp,? secure1.jsp,? secure2.jsp,? logout.jsp,? loginAction.jsp,?和 logoutAction.jsp。其中頁面home.jsp,? secure1.jsp,? secure2.jsp, 和 logout.jsp是不允許未經認證的用戶訪問的,也就是說,這些頁面包含了重要信息,在用戶登陸之前或者退出之后都不應該顯示在瀏覽器中。login.jsp頁面包含了用于用戶輸入用戶名和密碼的form。logout.jsp頁面包含了要求用戶確認是否退出的form。loginAction.jsp和logoutAction.jsp作為控制器分別包含了登陸和退出動作的代碼。

    第二個Web示例應用logoutSampleJSP2展示了如何糾正示例logoutSampleJSP1中的問題。但是第二個示例logoutSampleJSP2自身也是有問題的。在特定情況下,退出問題依然存在。

    第三個Web示例應用logoutSampleJSP3對logoutSampleJSP2進行了改進,比較妥善地解決了退出問題。

    最后一個Web示例logoutSampleStruts展示了JakartaStruts如何優雅地解決退出問題。

    注意:本文所附示例在最新版本的Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox和Avant瀏覽器上測試通過。



    二. Login action

    Brian Pontarelli的經典文章
    《J2EE Security: Container Versus Custom》 討論了不同的J2EE認證方法。文章同時指出,HTTP協議和基于form的認證方法并不能提供處理用戶退出問題的機制。因此,解決方法便是引入用戶自定義的安全實現機制,這就提供了更大的靈活性。

    在用戶自定義的認證方法中,普遍采用的方法是從用戶提交的form中獲得用戶輸入的認證信息,然后到諸如LDAP (lightweight directory access protocol)或關系數據庫(relational database management system, RDBMS)的安全域中進行認證。如果用戶提供的認證信息是有效的,登陸動作在HttpSession對象中保存某個對象。HttpSession存在著保存的對象則表示用戶已經登陸到Web應用當中。為了方便起見,本文所附的示例只在HttpSession中保存一個用戶名以表明用戶已經登陸。清單1是從loginAction.jsp頁面中節選的一段代碼以此講解登陸動作:




    Listing 1
    //...

    //initialize RequestDispatcher object; set forward to home page by default
    RequestDispatcher rd = request.getRequestDispatcher(
    "home.jsp" );

    //Prepare connection and statement

    rs = stmt.executeQuery(
    "select password from USER where userName = '" + userName + "'" );
    if (rs.next()) {
    //Query only returns 1 record in the result set;
    //Only 1
    password per userName which is also the primary key
    ????if (rs.getString(
    "password" ).equals(password)) { //If valid password

    ??????? session.setAttribute(
    "User" , userName); //Saves username string in the session object
    ??? }
    ??? else {
    //Password does not match, i.e., invalid user password
    ?????? ?request.setAttribute(
    "Error" , "Invalid password." );

    ?????? ?rd = request.getRequestDispatcher(
    "login.jsp"
    );
    ??? }
    }
    //No record in the result set, i.e., invalid username

    ??? else {

    ??????? request.setAttribute(
    "Error" , "Invalid user name." );
    ??????? rd = request.getRequestDispatcher(
    "login.jsp"
    );
    ??? }
    }

    //As a controller, loginAction.jsp finally either forwards to "login.jsp" or "home.jsp"

    rd.forward(request, response);
    //...

    				
    				
    		


    本文當中所附Web應用示例均以關系型數據庫作為安全域,但本問所講述的內容同樣適用于其他任何類型的安全域。



    三. Logout action

    退出動作包含刪除用戶名以及調用用戶的HttpSession對象的invalidate()方法。清單2是從loginoutAction.jsp中節選的一段代碼,以此說明退出動作:



    Listing 2
    //...

    session.removeAttribute(
    "User" );
    session.invalidate();
    //...

    						
    						
    				


    四. 阻止未經認證訪問受保護的JSP頁面

    從提交的form中獲取用戶提交的認證信息并經過驗證后,登陸動作僅僅在HttpSession對象中寫入一個用戶名。退出動作則剛好相反,它從HttpSession中刪除用戶名并調用HttpSession對象的invalidate()方法。為了使登陸和退出動作真正發揮作用,所有受保護的JSP頁面必須首先驗證HttpSession中包含的用戶名,以便確認用戶當前是否已經登陸。如果HttpSession中包含了用戶名,就說明用戶已經登陸,Web應用會將剩余的JSP頁中的動態內容發送給瀏覽器。否則,JSP頁將跳轉到登陸頁面,login.jsp。頁面home.jsp,? secure1.jsp,? secure2.jsp和 logout.jsp均包含清單3中的代碼段:


    Listing 3
    //...
    String userName = (String) session.getAttribute(
    "User" );
    if (null == userName) {
    ??? request.setAttribute(
    "Error" , "Session has ended. Please login."
    );
    ??? RequestDispatcher rd = request.getRequestDispatcher(
    "login.jsp"
    );
    ??? rd.forward(request, response);
    }
    //...

    //Allow the rest of the dynamic content in this JSP to be served to the browser
    //...
    						
    						
    				


    在這個代碼段中,程序從HttpSession中檢索username字符串。如果username字符串為空,Web應用則自動中止執行當前頁面并跳轉到登陸頁,同時給出錯誤信息“Session has ended. Please log in.”;如果不為空,Web應用繼續執行,把剩余的頁面提供給用戶,從而使JSP頁面的動態內容成為服務對象。



    五.運行logoutSampleJSP1

    運行logoutSampleJSP1將會出現如下幾種情形:

    ? 如果用戶沒有登陸,Web應用將會正確中止受保護頁面home.jsp,? secure1.jsp,? secure2.jsp和logout.jsp中動態內容的執行。也就是說,假如用戶并沒有登陸,但是在瀏覽器地址欄中直接敲入受保護JSP頁的地址試圖訪問,Web應用將自動跳轉到登陸頁面,同時顯示錯誤信息“Session has ended.Please log in.”

    ? 同樣的,當一個用戶已經退出,Web應用將會正確中止受保護頁面home.jsp,? secure1.jsp,? secure2.jsp和logout.jsp中動態內容的執行。也就是說,用戶退出以后,如果在瀏覽器地址欄中直接敲入受保護JSP頁的地址試圖訪問,Web應用將自動跳轉到登陸頁面,同時顯示錯誤信息“Session has ended.Please log in.”

    ? 用戶退出以后,如果點擊瀏覽器上的后退按鈕返回到先前的頁面,Web應用將不能正確保護受保護的JSP頁面——在Session銷毀后(用戶退出)受保護的JSP頁會重新顯示在瀏覽器中。然而,點擊該頁面上的任何鏈接,Web應用都會跳轉到登陸頁面,同時顯示錯誤信息“Session has ended.Please log in.”



    六. 阻止瀏覽器緩存
    ?
    上述問題的根源就在于現代大部分瀏覽器都有一個后退按鈕。當點擊后退按鈕時,默認情況下瀏覽器不會從Web服務器上重新獲取頁面,而是簡單的從瀏覽器緩存中重新載入頁面。這個問題并不僅限于基于Java(JSP/servlets/Struts) 的Web應用當中,在基于PHP (Hypertext Preprocessor)、ASP、(Active Server Pages)、和.NET的Web應用中也同樣存在。

    在用戶點擊后退按鈕之后,瀏覽器到Web服務器(一般來說)或者應用服務器(在java的情況下)再從服務器到瀏覽器這樣通常意義上的HTTP回路并沒有建立。僅僅只是用戶,瀏覽器和緩存之間進行了交互。所以即使受保護的JSP頁面,例如home.jsp,? secure1.jsp,? secure2.jsp和logout.jsp包含了清單3上的代碼,當點擊后退按鈕時,這些代碼也永遠不會執行的。

    緩存的好壞,真是仁者見仁智者見智。緩存事實上的確提供了一些便利,但這些便利通常只存在于靜態的HTML頁面或基于圖形或影像的頁面。而另一方面,Web應用通常是面向數據的。由于Web應用中的數據頻繁變更,所以與為了節省時間從緩存中讀取并顯示過期的數據相比,提供最新的數據顯得尤為重要!

    幸運的是,HTTP頭信息“Expires”和“Cache-Control”為應用程序服務器提供了一個控制瀏覽器和代理服務器上緩存的機制。HTTP頭信息Expires告訴代理服務器它的緩存頁面何時將過期。HTTP1.1規范中新定義的頭信息Cache-Control在Web應用當中可以通知瀏覽器不緩存任何頁面。當點擊后退按鈕時,瀏覽器發送Http請求道應用服務器以便獲取該頁面的最新拷貝。如下是使用Cache-Control的基本方法:

    ? no-cache:強制緩存從服務器上獲取該頁面的最新拷貝
    ? no-store: 在任何情況下緩存不保存該頁面

    HTTP1.0規范中的Pragma:no-cache等同于HTTP1.1規范中的Cache-Control:no-cache,同樣可以包含在頭信息中。

    通過使用HTTP頭信息的cache控制,第二個示例應用logoutSampleJSP2解決了logoutSampleJSP1的問題。logoutSampleJSP2與logoutSampleJSP1不同表現在如下代碼段中,這一代碼段加入進所有受保護的頁面中:




    //...
    response.setHeader(
    "Cache-Control" , "no-cache" ); //Forces caches to obtain a new copy of the page from the origin server
    response.setHeader(
    "Cache-Control" , "no-store" ); //Directs caches not to store the page under any circumstance
    response.setDateHeader(
    "Expires" , 0); //Causes the proxy cache to see the page as "stale"
    response.setHeader(
    "Pragma" , "no-cache" ); //HTTP 1.0 backward compatibility
    String userName = (String) session.getAttribute(
    "User" );
    if (null == userName) {
    ??? request.setAttribute(
    "Error" , "Session has ended. Please login."
    );
    ??? RequestDispatcher rd = request.getRequestDispatcher(
    "login.jsp"
    );
    ??? rd.forward(request, response);
    }
    //...

    						
    						
    				


    通過設置頭信息和檢查HttpSession對象中的用戶名來確保瀏覽器不會緩存JSP頁面。同時,如果用戶未登陸,JSP頁面的動態內容不會發送到瀏覽器,取而代之的將是登陸頁面login.jsp。



    七. 運行logoutSampleJSP2

    運行Web示例應用logoutSampleJSP2后將會看到如下結果:

    ? 當用戶退出后試圖點擊后退按鈕,瀏覽器不會重新顯示受保護的頁面,它只會顯示登陸頁login.jsp同時給出提示信息Session has ended. Please log in.

    ? 然而,當按了后退按鈕返回的頁是處理用戶提交數據的頁面時,IE和Avant瀏覽器將彈出如下信息提示:

    ?????????? 警告:頁面已過期
    ?????????? The page you requested was created using information you submitted in a form.?This page is?no longer available. As a security precaution, Internet Explorer does not automatically? resubmit your information for you.

    Mozilla和FireFox瀏覽器將會顯示一個對話框,提示信息如下:

    ??????????? The page you are trying to view contains POSTDATA that has expired from cache. If you? resend the data, any action from the form carried out (such as a search or online purchase) will be repeated. To resend the data, click OK. Otherwise, click Cancel.

    在IE和Avant瀏覽器中選擇刷新或者在Mozilla和FireFox瀏覽器中選擇重新發送數據后,前一個JSP頁面將重新顯示在瀏覽器中。顯然的,這病不是我們所想看到的因為它違背了logout動作的目的。發生這一現象時,很可能是一個惡意用戶在嘗試獲取其他用戶的數據。然而,這個問題僅僅出現在點擊后退按鈕后,瀏覽器返回到一個處理POST請求的頁面。



    八. 記錄最后登陸時間

    上述問題的發生是因為瀏覽器重新提交了其緩存中的數據。這本文的例子中,數據包含了用戶名和密碼。盡管IE瀏覽器給出了安全警告信息,但事實上瀏覽器此時起到了負面作用。

    為了解決logoutSampleJSP2中出現的問題,logoutSampleJSP3的login.jsp除了包含username和password的之外,還增加了一個稱作lastLogon的隱藏表單域,此表單域將會動態的被初始化為一個long型值。這個long型值是通過調用System.currentTimeMillis()獲取到的自1970年1月1日以來的毫秒數。當login.jsp中的form提交時,loginAction.jsp首先將隱藏域中的值與用戶數據庫中的lastLogon值進行比較。只有當lastLogon表單域中的值大于數據庫中的值時Web應用才認為這是個有效的登陸。

    為了驗證登陸,數據庫中lastLogon字段必須用表單中的lastLogon值進行更新。上例中,當瀏覽器重復提交緩存中的數據時,表單中的lastLogon值不比數據庫中的lastLogon值大,因此,loginAction將跳轉到login.jsp頁面,并顯示如下錯誤信息“Session has ended.Please log in.”清單5是loginAction中節選的代碼段:




    清單5
    //...
    RequestDispatcher rd = request.getRequestDispatcher(
    "home.jsp" ); //Forward to homepage by default
    //...
    if (rs.getString(
    "password" ).equals(password)) { //If valid password
    ??? long lastLogonDB = rs.getLong(
    "lastLogon" );
    ??? if (lastLogonForm > lastLogonDB) {
    ??????? session.setAttribute(
    "User" , userName); //Saves username string in the session object

    ??????? stmt.executeUpdate(
    "update USER set lastLogon= " + lastLogonForm + " where userName = '" + userName + "'" );
    ??? }
    ??? else {
    ??????? request.setAttribute(
    "Error" , "Session has ended. Please login."
    );
    ??????? rd = request.getRequestDispatcher(
    "login.jsp"
    ); }
    ??? }
    else {
    //Password does not match, i.e., invalid user password

    ??? request.setAttribute(
    "Error" , "Invalid password." );
    ??? rd = request.getRequestDispatcher(
    "login.jsp"
    );
    }
    //...

    rd.forward(request, response);
    //...
    						
    						
    				


    為了實現上述方法,你必須記錄每個用戶的最后登陸時間。對于采用關系型數據庫安全域來說,這點可以可以通過在某個表中加上lastLogin字段輕松實現。雖然對LDAP以及其他的安全域來說需要稍微動下腦筋,但最后登陸方法很顯然是可以實現的。

    表示最后登陸時間的方法有很多。示例logoutSampleJSP3利用了自1970年1月1日以來的毫秒數。這個方法即使在許多人在不同瀏覽器中用一個用戶賬號登陸時也是可行的。



    九. 運行logoutSampleJSP3

    運行示例logoutSampleJSP3將展示如何正確處理退出問題。一旦用戶退出,點擊瀏覽器上的后退按鈕在任何情況下都不會在瀏覽器中顯示受保護的JSP頁面。這個示例展示了如何正確處理退出問題而不需要對用戶進行額外的培訓。

    為了使代碼更簡練有效,一些冗余的代碼可以剔除。一種途徑就是把清單4中的代碼寫到一個單獨的JSP頁中,其他JSP頁面可以通過標簽
    <jsp:include>進行使用



    十. Struts框架下的退出實現

    與直接使用JSP或JSP/servlets進行Web應用開發相比,另一個更好的可選方案是使用Struts。對于一個基于Struts的Web應用來說,添加一個處理退出問題的框架可以優雅地不費氣力的實現。這歸功于Struts是采用MVC設計模式的,因此可以將模型和視圖代碼清晰的分離。另外,Java是一個面向對象的語言,支持繼承,可以比JSP中的腳本更為容易地實現代碼重用。對于Struts來說,清單4中的代碼可以從JSP頁面中移植到Action類的execute()方法中。

    此外,我們還可以定義一個繼承Struts Action類的Action基類,其execute()方法中包含了類似清單4中的代碼。通過繼承,其他Action類可以繼承基本類中的通用邏輯來設置HTTP頭信息以及檢索HttpSession對象中的username字符串。這個Action基類是一個抽象類并定義了一個抽象方法executeAction()。所有繼承自Action基類的子類都必須實現exectuteAction()方法而不是覆蓋它。通過繼承這一機制,所有繼承自Action基類的子類都不必再擔心退出代碼接口。(plumbing實在不知道怎么翻譯了,^+^,高手幫幫忙??!原文:With this inheritance hierarchy in place, all of the base Action's subclasses no longer need to worry about any plumbing logout code.)。他們將只包含正常的業務邏輯代碼。清單6是基類的部分代碼:



    清單6
    publicabstractclass BaseAction extends Action {
    ??? public ActionForward execute(ActionMapping mapping, ActionForm form,
    ???????????????????????????????????????????????? HttpServletRequest request, HttpServletResponse response)?
    ???????????????throws IOException, ServletException {

    response.setHeader(
    "Cache-Control" , "no-cache" ); //Forces caches to obtain a new copy of the page from the origin server
    response.setHeader(
    "Cache-Control" , "no-store" ); //Directs caches not to store the page under any circumstance
    response.setDateHeader(
    "Expires" , 0); //Causes the proxy cache to see the page as "stale"
    response.setHeader(
    "Pragma" , "no-cache" ); //HTTP 1.0 backward compatibility

    if (!this.userIsLoggedIn(request)) {
    ??? ActionErrors errors = new ActionErrors();

    ??? errors.add(
    "error" , new ActionError( "logon.sessionEnded" ));
    ??? this.saveErrors(request, errors);

    ??? return mapping.findForward(
    "sessionEnded"
    );
    }

    return executeAction(mapping, form, request, response);
    }

    protectedabstract ActionForward executeAction(ActionMapping mapping,? ActionForm?form,?HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException;

    privateboolean userIsLoggedIn(HttpServletRequest request) {
    if (request.getSession().getAttribute(
    "User"
    ) == null) {
    ??? return false;
    }

    return true;
    }
    }
    						
    						
    				


    清單6中的代碼與清單4中的很相像,唯一區別是用ActionMapping findForward替代了RequestDispatcher forward。清單6中,如果在HttpSession中未找到username字符串,ActionMapping對象將找到名為sessionEnded的forward元素并跳轉到對應的path。如果找到了,子類通過實現executeAction()方法,將執行他們自己的業務邏輯。因此,在struts-web.xml配置文件中為所有繼承自Action基類的子類聲明個一名為sessionEnded的forward元素并將其指向login.jsp是至關重要的。清單7以secure1 action闡明了這樣一個聲明:


    清單7
    <action path=
    "/secure1"
    type=
    "com.kevinhle.logoutSampleStruts.Secure1Action"
    scope=
    "request" >
    <forward name=
    "success" path= "/WEB-INF/jsps/secure1.jsp"
    />
    <forward name=
    "sessionEnded" path= "/login.jsp"
    />
    </action>

    						
    						
    				


    繼承自BaseAction類的子類Secure1Action實現了executeAction()方法而不是覆蓋它。Secure1Action類不需要執行任何退出代碼,如清單8:



    清單8
    publicclass Secure1Action extends BaseAction {
    ??? public ActionForward executeAction(ActionMapping mapping, ActionForm form,
    ?????????????????????????????????????????????????????????? HttpServletRequest request, HttpServletResponse response)
    ?????????????? throws IOException, ServletException {

    ?????????????? HttpSession session = request.getSession();?
    ???????????????return (mapping.findForward(
    "success" ));
    ?????????}
    }
    						
    						
    				


    上面的解決方案是如此的優雅有效,它僅僅只需要定義一個基類而不需要額外的代碼工作。將通用的行為方法寫成一個繼承StrutsAction的基類是者的推薦的,而且這是許多Struts項目的共同經驗。




    十一. 局限性

    上述解決方案對JSP或基于Struts的Web應用都是非常簡單而實用的,但它還是有某些局限。在我看來,這些局限并不是至關緊要的。

    ??? 通過取消與瀏覽器后退按鈕有關的緩存機制,一旦用戶離開頁面而沒有對數據進行提交,那么頁面將會丟失所有輸入的數據。即使點擊瀏覽器的后退按鈕返回到剛才的頁面也無濟于事,因為瀏覽器會從服務器獲取新的空白頁面顯示出來。一種可能的方法并不是阻止這些JSP頁面包含數據數據表格。在基于JSP的解決方案當中,那些JSP頁面可以刪除在清單4中的代碼。在基于Struts的解決方案當中,Action類需要繼承自Struts的Action類而非BaseAction類。

    ???上面講述的方法在Opera瀏覽器中不能工作。事實上沒有適用于Opera瀏覽器的解決方案,因為Opera瀏覽器與2616 Hypertext Transfer Protocol—HTTP/1.1緊密相關。Section 13.13 of?RFC 2616 states:??????????
    User agents often have history mechanisms, such as "Back" buttons and history lists, which can be used to redisplay an entity retrieved earlier in a session.

    History mechanisms and caches are different. In particular history mechanisms SHOULD NOT try to show a semantically transparent view of the current state of a resource. Rather, a history mechanism is meant to show exactly what the user saw at the time when the resource was retrieved.

    幸運的是,使用微軟的IE和基于Mozilla的瀏覽器用戶多余Opera瀏覽器。上面講述的解決方案對大多數用戶來說還是有幫助的。另外,無論是否使用上述的解決方案,Opera瀏覽器仍然存在用戶退出問題,就Opera來說沒有任何改變。然而,正如RFC2616中所說,通過像上面一樣設置頭文件指令,當用戶點擊一個鏈接時,Opera瀏覽器不會從緩存中獲取頁面。




    十二. 結論

    這篇文章講述了處理退出問題的解決方案,盡管方案簡單的令人驚訝,但在所有情況下都能有效地工作。無論是對JSP還是Struts,所要做的不過是寫一段不超過50行的代碼以及一個記錄用戶最后登陸時間的方法。在有密碼保護的Web應用中使用這些方案能夠確保在任何情況下用戶的私人數據不致泄露,同時,也能增加用戶的經驗。

    About the author
    [i]Kevin H. Le[/i] has more than 12 years of experience in software development. In the first half of his career, his programming language of choice was C++. In 1997, he shifted his focus to Java. He has engaged in and successfully completed several J2EE and EAI projects as both developer and architect. In addition to J2EE, his current interests now include Web services and SOA. More information on Kevin can be found on his Website http://kevinhle.com.

    posted @ 2006-08-24 15:56 Binary 閱讀(317) | 評論 (0)編輯 收藏

    區分eclipse中的兩種JRE

    CowNew 開源團隊網站 www.cownew.com

    論壇 http://www.cownew.com/newpeng/?

    轉載請保留此信息

    今天一個CownewStudio的使用者通過QQ問我他的Eclipse安裝CownewStudio以后在eclipse中可以看到studio,但是運行的時候提示類加載錯誤。因為CownewStudio目前的版本只支持JDK5,所以我詢問他Eclipse使用的是不是1.4的JRE,但是他確認它用的就是1.5的。
    后來經過實驗,我確認還是JRE版本的問題,他就把他的Eclipse截圖發給了我,以證明他用的是JDK1.5,但是我發現他發過來的圖片工程編譯器配置對話框的。哈哈,我終于明白了,讓他把Eclipse的配置詳細信息(“幫助”=》“關于Eclipse”=》“配置詳細信息”)發過來,果然:
    -vm
    c:\programe\jdk1.4.2\jre\bin\javaw.exe

    原來他裝了多個版本的JDK。我要他把JDK1.5目錄下的JRE目錄拷貝到eclipse安裝目錄下,然后重啟Eclipse,一切工作正常了。
    其實這是很多剛剛接觸Eclipse、甚至用了好長時間Eclipse的開發人員經常犯的錯,也就是把Eclipse運行時的JRE與工作空間中項目所用的JRE版本弄混亂。
    Eclipse也是一個普通的Java程序,因此必須有一個JRE做為運行環境。如果你的機器上沒有安裝任何JRE(或者JDK,本文不做二者的區分),那么點擊eclipse.exe就會報錯說找不到JRE。此時可以安裝一個JRE、或者直接把JRE目錄拷貝到eclipse安裝目錄下。
    在Eclipse的每個項目中可以為項目指定不同的JRE版本,比如A項目使用JDK1.4編譯,B項目使用JDK1.5編譯。這個JDK版本是和Eclipse運行JRE沒有直接關系的。
    項目的JDK版本是很容易修改的,那么任何指定Eclipse啟動所用的JRE呢?
    Eclipse啟動的時候找JRE的順序是:如果eclipse.ini中配置了-vm參數,那么則使用這個參數指定的JRE;否則就去查看eclipse安裝目錄下是否有JRE文件夾,如果有的話就使用這個JRE;否則的話就去系統中查找安裝的JRE,如果還找不到的話就報錯。
    所以如果不想卸載掉其他的JDK的話,可以有兩種方式:(1)直接把要使用的JRE文件夾拷貝到Eclipse目錄下,這是懶人常用的方法(2)修改eclipse.ini文件,添加-vm參數,指定要運行的虛擬機的地址,使用 -vm 命令行自變量例子:-vm c:\jre\bin\javaw.exe

    posted @ 2006-08-24 15:54 Binary 閱讀(236) | 評論 (0)編輯 收藏

    對象傳遞和信息完整性

    ??? 很少有孤立存在的實體對象,它們之間總是會有所關聯。對象們因其職責而分離,又因其聯系而聚合。而我們在使用對象時,往往不需要把對象及其聚合的所有其他對象一次性全部初始化,部分的對象聚合足以提供足夠的信息了。這時候,我們使用的對象的信息是不完整的。

    ??? 當具有不完整信息的對象被做為參數傳遞時,很可能導致我們對對象失去控制。部分對象的聚合在不同情景下的切換可能導致編程上的錯誤,而且對系統的維護帶來負面的影響。由于對象職責的分割,對象本身無法理解這個問題,我們無法通過對對象本身進行處理(如增加職責)來消除這種不良影響。如何使用對象的部分信息是由使用者根據情景要求決定的,編碼人員往往對使用情景沒有深刻的認識。在連續對應不同情景的處理鏈中,容易導致編碼錯誤。如下:

    ?1//?Entity?objects.
    ?2public?class?Obj?{
    ?3??private?ObjRef1?ref1;
    ?4??Private?ObjRef2?ref2;
    ?5
    ?6??public?Obj()?{
    ?7??}
    ?8
    ?9??public?Obj(ObjRef1?ref1)?{
    10????this.ref1?=?ref1;
    11??}
    12
    13??public?Obj(ObjRef1?ref1,?ObjRef2?ref2)?{
    14????this.ref1?=?ref1;
    15????this.ref2?=?ref2;
    16??}
    17??//?Accessors?omitted.
    18}?//?ObjRef1,?ObjRef2?omitted.
    19
    20// Process1
    21public?class Prs1?{
    22??public?Obj?method1()?{
    23????Obj?obj?=?new?Obj();
    24????obj.setObjRef1(loadObjRef1());
    25????//?Do?something?to?obj.
    26????return?obj;
    27??}
    ???? // Load method omitted.
    ???? private ObjRef1 loadObjRef1() {
    ?????? // ......
    ???? }
    28}
    29
    30// Process2
    31public?class Prs2?{
    32??public?void?method2()?{
    33????Obj?obj?=?new?Clt1().method1();
    34????//?Do?something?to?obj.ref2.?Coding?error?may?be?found?until?debug?phase.
    35??}
    36}

    ??? 在Process1.method1方法中持久化Obj,在Process2.method2中自行初始化Obj。這個方案面向對象并解決了一致性問題,但增加性能成本。在Process1.method1中loadObjRef2,不夠面向對象,增加了維護成本。
    ???
    ??? 盡可能不使用這種鏈式的處理,代之以扁平的、可以總控Obj的方式:
    ???
    ?1public?class?Process?{
    ?2??public?void?proceed()?{
    ?3????//?Need?to?change?Prs1,?Prs2's?method?signatures.
    ?4????Obj?obj?=?
    ?5????obj?=?new?Prs1().method1(obj);
    ?6????obj.setObjRef2();
    ?7????obj?=?new?Prs2().method2(obj);
    ?8????
    ?9????//?Future?processes?can?be?inserted?anywhere?inside?this?method.
    10??}
    11}


    ??? 對于樹形結構或整體-部分結構,有一個統一的處理合理的,但內部的復雜性還是很高。

    posted @ 2006-08-24 15:44 Binary 閱讀(281) | 評論 (0)編輯 收藏

    Java線程安全

    一直不敢寫點什么,是因為戰戰兢兢,生怕寫的不好甚至寫錯了會誤人子弟。隨筆可以隨便寫一下,不用太過計較,可是技術從來都要不得半點馬虎,差之毫厘,謬以千里??!但敝帚自珍又不是我的風格,雖然文筆不好,也要勉為其難了。廢話少說,進入正題。

    ?

    ?????? 從我開始接觸 Java 的多線程起就總是覺得書上講的不是那么清楚。不是說讀完了不會寫,而是對寫出來的多線程代碼懵懵懂懂,不知道每一句會有什么影響,心里感覺忐忑。后來仔細研讀 Java 語言規范后,才慢慢搞明白一些細節。我主要想說的,也就是這些經驗吧。

    ?

    ?????? 首先要搞清楚的是線程的共享資源,共享資源是多線程中每個線程都要訪問的類變量或實例變量,共享資源可以是單個類變量或實例變量,也可以是一組類變量或實例變量。多線程程序可以有多個共享資源。下面描述他們之間的一對多關系( * 表示多):

    ??????

    ???????????????????? 多線程程序( 1 ---- 共享資源( * ---- 類變量或實例變量( 1…*

    ?

    只有類變量和實例變量可以成為共享資源,細分如下:

    <!--[if !supportLists]-->1.?????? <!--[endif]-->實現線程的類(繼承Thread類、實現Throwable接口的類)的類變量、實例變量。

    <!--[if !supportLists]-->2.?????? <!--[endif]-->實現線程的類的類變量、實例變量的類變量、實例變量,可以不規范的寫為:TreadClass.ClassOrInstanceVar[.ClassOrInstanceVar]*,[]*的內容表示無限可重復。

    <!--[if !supportLists]-->3.?????? <!--[endif]-->不是實現線程的類,但其對象可能是線程的類變量或實例變量。如Servlet、EJB。這些類的類變量和實例變量,不規范的寫為:ServletOrEJB.ClassOrInstanceVar[.ClassOrInstanceVar]*

    <!--[if !supportLists]-->4.?????? <!--[endif]-->特別注意:局部變量、做為參數傳遞的非類變量、非實例變量不是共享資源。

    ?

    那么什么是線程安全呢?關于這個問題我在網上百度了一下(沒辦法,有時候 GOOGLE 用不了),發現不少人在問這個問題,也有不少錯誤的理解。所以我給出一個較容易理解的解釋:在線程中使用共享資源時,能夠保證共享資源在任何時候都是原子的、一致的,這樣的線程就是線程安全的線程。還不太理解?沒有關系,慢慢解釋。

    ?

    首先來介紹一下共享資源的類型(這是我自己分類的,為了后文好解釋),共享資源從其類型可以分為三類(下文講到變量一律指類變量或實例變量,不再特別指出):

    <!--[if !supportLists]-->1.?????? <!--[endif]-->獨立的基本類型共享資源,如一個簡單的int變量,例:

    public class Cls1 {

    ?????? private int a;

    ?????? public int getA(){return a;}

    ?????? public void setA(int a){this.a = a;}

    }

    可以看到 a 沒有任何依賴。

    public class Cls2{

    ?????? private int a;

    ?????? private int b;

    ?????? private int c;

    ?????? // 沒有對 a 的訪問方法, a Cls 外不可見。

    }

    假設上面類中 b 、 c 都不依賴 a ,則 a 是這種類型。

    ?

    <!--[if !supportLists]-->2.?????? <!--[endif]-->相互依賴的基本類型共享資源,一個類中的幾個基本類型變量互相依賴,但從對象設計的角度又不能單獨把這幾個變量設計成一個類。

    假設上例 Cls2 中的 b c 互相依賴,則屬此種情況。

    <!--[if !supportLists]-->3.?????? <!--[endif]-->64位的基本類型變量。這個比較特殊,因為某些機器上64變量會分成兩個32位的操作,所以和1不一樣。如double、long類型。

    <!--[if !supportLists]-->4.?????? <!--[endif]-->類類型的共享資源。如下例中的obj

    public class Cls3{

    ?????? private SomeObj obj;

    }

    public class SomeObj{

    ?????? private int a;

    ?????? private int b;

    }

    ?

    ?????? 其次來看看什么是原子性、一致性。其實在這里我借用了事務 ACID 屬性的 A C ,熟悉的朋友就不用我廢話了。所謂原子性,是指一個共享資源的所有屬性在任何時刻都是一起變化、密不可分的;所謂一致性,是指一個共享資源的所有屬性在變化之后一定會達到一個一致的狀態。

    ?

    ?????? 最后根據上述四種共享資源類型,來看看如何做到線程安全。

    ?

    <!--[if !supportLists]-->1.?????? <!--[endif]-->不用做什么,只一個獨立的變量,任何時候它都是原子、一致的。

    <!--[if !supportLists]-->2.?????? <!--[endif]-->使用synchronized關鍵字,保證幾個變量被一起修改、一起讀取。

    <!--[if !supportLists]-->3.?????? <!--[endif]-->使用volatile關鍵字,然后就和1一樣了。

    <!--[if !supportLists]-->4.?????? <!--[endif]-->2一樣處理。

    ?

    當對訪問共享資源的方法不同時使用 synchronized 關鍵字時,是什么樣一種情況呢?這是需要特別注意的,這樣不能保證線程安全!看看下面例子的運行結果就知道了(自己運行啊,我不貼結果了):

    /**

    ?* $Author: $

    ?* $Date: $

    ?* $Revision: $

    ?* $History: $

    ?*

    ?* Created by feelyou, at time 22:31:53 , 2005-11-16.

    ?*/

    ?

    public class TestThread extends Thread {

    ?

    ? private int a = 0;

    ? private int b = 0;

    ?

    ? public static void main(String[] args) {

    ??? TestThread test = new TestThread();

    ??? for (int i = 0; i < 10; i++) {

    ????? Thread thread = new Thread(test, "thread-" + i);

    ????? thread.start();

    ??? }

    ? }

    ?

    ? public synchronized void doWrite() {

    ??? a++;

    ??? try {

    ????? sleep((int)(Math.random()*100));

    ??? }

    ??? catch (InterruptedException e) {

    ??? }

    ??? b++;

    ??? try {

    ????? sleep((int)(Math.random()*100));

    ??? }

    ??? catch (InterruptedException e) {

    ??? }

    ? }

    ?

    ? public void print() {

    ??? System.out.println("" + Thread.currentThread().getName() + ":a:" + a);

    ??? System.out.println("" + Thread.currentThread().getName() + ":b:" + b);

    ? }

    ?

    ? public void run() {

    ??? super.run();??? //To change body of overridden methods use File | Settings | File Templates.

    ??? for (int i = 0; i < 10; i++) {

    ????? doWrite();

    ????? print();

    ??? }

    ? }

    ?

    ? public synchronized void start() {

    ??? super.start();??? //To change body of overridden methods use File | Settings | File Templates.

    ? }

    }

    ?

    ThreadLocal ThreadLocal 對于線程安全還是很有用的,如果資源不是共享的,那么應該使用 ThreadLocal ,但如果確實需要在線程間共享資源, ThreadLocal 就沒有用了!

    ?

    最后,來一個完整的線程安全的例子:

    /**

    ?* $Author: $

    ?* $Date: $

    ?* $Revision: $

    ?* $History: $

    ?*

    ?* Created by feelyou, at time 22:31:53 , 2005-11-16.

    ?*/

    ?

    public class TestThread extends Thread {

    ?

    ? private int a = 0; // 獨立的共享資源

    ? private int b = 0; //b 、 c 互相依賴

    ? private int c = 0;

    ? private volatile long d = 0L; //64

    //? private SomeObj obj = new SomeObj(); // 對象類型,大家自己寫吧,我就不寫了。

    ?

    ? public static void main(String[] args) {

    ??? TestThread test = new TestThread();

    ??? for (int i = 0; i < 10; i++) {

    ????? Thread thread = new Thread(test, "thread-" + i);

    ????? thread.start();

    ??? }

    ? }

    ?

    ? public synchronized void doWrite() {

    ??? b++;

    ??? try {

    ????? sleep((int)(Math.random()*100));

    ??? }

    ??? catch (InterruptedException e) {

    ??? }

    ??? c++;

    ??? try {

    ?? ???sleep((int)(Math.random()*100));

    ??? }

    ??? catch (InterruptedException e) {

    ??? }

    ? }

    ?

    ? public synchronized void print() {

    ??? System.out.println("" + Thread.currentThread().getName() + ":b:" + b);

    ??? System.out.println("" + Thread.currentThread().getName() + ":c:" + c);

    ? }

    ?

    ? private void setA(int a) {

    ? ??? this.a = a;

    ? }

    ?

    ? private int getA() {

    ? ??? return a;

    ? }

    ?

    ? public long getD() {

    ? ??? return d;

    ? }

    ?

    ? public void setD(long d) {

    ? ??? this.d = d;

    ? }

    ?

    ? public void run() {

    ??? super.run();??? //To change body of overridden methods use File | Settings | File Templates.

    ??? for (int i = 0; i < 10; i++) {

    ????? doWrite();

    ????? print();

    ????? setA(i);

    ????? System.out.println(getA());

    ????? setD(18456187413L * i);

    ????? System.out.println(getD());

    ??? }

    ? }

    ?

    ? public synchronized void start() {

    ??? super.start();??? //To change body of overridden methods use File | Settings | File Templates.

    ? }

    }

    posted @ 2006-08-24 15:42 Binary 閱讀(429) | 評論 (0)編輯 收藏

    Hibernate 延遲初始化錯誤(ERROR LazyInitializer)是如何產生的?

    摘要:
    延遲初始化錯誤是運用Hibernate開發項目時最常見的錯誤。如果對一個類或者集合配置了延遲檢索策略,那么必須當代理類實例或代理集合處于持久化狀態(即處于Session范圍內)時,才能初始化它。如果在游離狀態時才初始化它,就會產生延遲初始化錯誤。

    延遲初始化錯誤(ERROR LazyInitializer)是如何產生的?

    選自<<精通Hibernate:Java對象持久化技術詳解>> 作者:孫衛琴 來源:www.javathinker.org

    延遲初始化錯誤是運用Hibernate開發項目時最常見的錯誤。如果對一個類或者集合配置了延遲檢索策略,那么必須當代理類實例或代理集合處于持久化狀態(即處于Session范圍內)時,才能初始化它。如果在游離狀態時才初始化它,就會產生延遲初始化錯誤。

    下面把Customer.hbm.xml文件的<class>元素的lazy屬性設為true,表示使用延遲檢索策略:
    <class name="mypack.Customer" table="CUSTOMERS" lazy="true">
    當執行Session的load()方法時,Hibernate不會立即執行查詢CUSTOMERS表的select語句,僅僅返回Customer類的代理類的實例,這個代理類具由以下特征:
    (1) 由Hibernate在運行時動態生成,它擴展了Customer類,因此它繼承了Customer類的所有屬性和方法,但它的實現對于應用程序是透明的。
    (2) 當Hibernate創建Customer代理類實例時,僅僅初始化了它的OID屬性,其他屬性都為null,因此這個代理類實例占用的內存很少。
    (3)當應用程序第一次訪問Customer代理類實例時(例如調用customer.getXXX()或customer.setXXX()方法), Hibernate會初始化代理類實例,在初始化過程中執行select語句,真正從數據庫中加載Customer對象的所有數據。但有個例外,那就是當應用程序訪問Customer代理類實例的getId()方法時,Hibernate不會初始化代理類實例,因為在創建代理類實例時OID就存在了,不必到數據庫中去查詢。

    提示:Hibernate采用CGLIB工具來生成持久化類的代理類。CGLIB是一個功能強大的Java字節碼生成工具,它能夠在程序運行時動態生成擴展Java類或者實現Java接口的代理類。關于CGLIB的更多知識,請參考:
    http://cglib.sourceforge.net/。
    以下代碼先通過Session的load()方法加載Customer對象,然后訪問它的name屬性:
    tx = session.beginTransaction();
    Customer customer=(Customer)session.load(Customer.class,new Long(1));
    customer.getName();tx.commit();

    在運行session.load()方法時Hibernate不執行任何select語句,僅僅返回Customer類的代理類的實例,它的OID為1,這是由load()方法的第二個參數指定的。當應用程序調用customer.getName()方法時,Hibernate會初始化Customer代理類實例,從數據庫中加載Customer對象的數據,執行以下select語句:
    select * from CUSTOMERS where ID=1;select * from ORDERS where CUSTOMER_ID=1;

    當<class>元素的lazy屬性為true,會影響Session的load()方法的各種運行時行為,下面舉例說明。

    1.如果加載的Customer對象在數據庫中不存在,Session的load()方法不會拋出異常,只有當運行customer.getName()方法時才會拋出以下異常:
    ERROR LazyInitializer:63 - Exception initializing proxynet.sf.hibernate.ObjectNotFoundException: No row with the given identifier exists: 1, of class: mypack.Customer

    2.如果在整個Session范圍內,應用程序沒有訪問過Customer對象,那么Customer代理類的實例一直不會被初始化,Hibernate不會執行任何select語句。以下代碼試圖在關閉Session后訪問Customer游離對象:
    tx = session.beginTransaction();
    Customer customer=(Customer)session.load(Customer.class,new Long(1));
    tx.commit();
    session.close();
    customer.getName();

    由于引用變量customer引用的Customer代理類的實例在Session范圍內始終沒有被初始化,因此在執行customer.getName()方法時,Hibernate會拋出以下異常:
    ERROR LazyInitializer:63 - Exception initializing proxynet.sf.hibernate.HibernateException:
    Could not initialize proxy - the owning Session was closed
    由此可見,Customer代理類的實例只有在當前Session范圍內才能被初始化。

    3.net.sf.hibernate.Hibernate類的initialize()靜態方法用于在Session范圍內顯式初始化代理類實例,isInitialized()方法用于判斷代理類實例是否已經被初始化。例如:
    tx = session.beginTransaction();
    Customer customer=(Customer)session.load(Customer.class,new Long(1));
    if(!Hibernate.isInitialized(customer)) Hibernate.initialize(customer);
    tx.commit();
    session.close();
    customer.getName();
    以上代碼在Session范圍內通過Hibernate類的initialize()方法顯式初始化了Customer代理類實例,因此當Session關閉后,可以正常訪問Customer游離對象。

    4.當應用程序訪問代理類實例的getId()方法時,不會觸發Hibernate初始化代理類實例的行為,例如:
    tx = session.beginTransaction();
    Customer customer=(Customer)session.load(Customer.class,new Long(1));
    customer.getId();
    tx.commit();
    session.close();
    customer.getName();
    當應用程序訪問customer.getId()方法時,該方法直接返回Customer代理類實例的OID值,無需查詢數據庫。由于引用變量 customer始終引用的是沒有被初始化的Customer代理類實例,因此當Session關閉后再執行customer.getName()方法, Hibernate會拋出以下異常:
    ERROR LazyInitializer:63 - Exception initializing proxynet.sf.hibernate.HibernateException:
    Could not initialize proxy - the owning Session was closed

    posted @ 2006-08-24 15:25 Binary 閱讀(442) | 評論 (0)編輯 收藏

    僅列出標題
    共8頁: 上一頁 1 2 3 4 5 6 7 8 下一頁 
    主站蜘蛛池模板: 亚洲欧美日韩一区二区三区| 亚洲综合在线成人一区| 国产亚洲综合网曝门系列| 日韩亚洲人成在线综合日本| 久久精品夜色国产亚洲av| 亚洲国产精品久久久久秋霞影院| 77777午夜亚洲| 一本久久免费视频| 久久久久国产精品免费看| 五月婷婷综合免费| 免费v片在线观看无遮挡| 好看的电影网站亚洲一区| 亚洲av无码片区一区二区三区| 国产精品亚洲一区二区三区| 99视频在线观看免费| 三年片在线观看免费大全| 亚洲Av无码国产情品久久| 亚洲伦理一区二区| 亚洲成在人线在线播放无码| 青青操在线免费观看| 国产精品久久久久免费a∨| 亚洲国产香蕉人人爽成AV片久久| 亚洲国产成人私人影院| 亚洲精品无码永久在线观看男男 | 亚洲日韩精品无码专区加勒比☆| 国产天堂亚洲精品| 一级毛片免费不卡在线| 国产乱子影视频上线免费观看| 久久精品国产亚洲夜色AV网站| 亚洲中文字幕无码亚洲成A人片| 72pao国产成视频永久免费| 4hu四虎最新免费地址| 国产黄色一级毛片亚洲黄片大全| 亚洲色成人网一二三区| 最新亚洲人成无码网站| 精品一区二区三区免费毛片爱| 日本高清免费中文字幕不卡| 亚洲另类激情综合偷自拍| 污视频网站免费观看| 亚洲性线免费观看视频成熟 | 日韩免费福利视频|