<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 on 2006-08-24 17:33 Binary 閱讀(277) 評論(0)  編輯  收藏 所屬分類: j2se

    主站蜘蛛池模板: 亚洲人配人种jizz| 亚洲国产综合精品中文字幕 | 亚洲人成电影亚洲人成9999网 | 亚洲av中文无码乱人伦在线r▽| 成年大片免费视频播放一级| 亚洲精品成人区在线观看| 日本视频免费观看| 久久久久亚洲AV综合波多野结衣| 国产精品永久免费视频| 亚洲尤码不卡AV麻豆| 国产午夜精品久久久久免费视| 亚洲国产精品久久久久婷婷软件 | 日本亚洲中午字幕乱码| 伊人久久亚洲综合影院| 国产成人精品免费视频网页大全| 亚洲自偷自拍另类12p| 黄色一级视频免费| 久久伊人亚洲AV无码网站| 日韩电影免费在线观看网站| 亚洲视频手机在线| 成人无码区免费A∨直播| 亚洲国产另类久久久精品小说| 亚洲欧美国产国产综合一区| 95老司机免费福利| 中文字幕不卡亚洲| 久99久精品免费视频热77| 亚洲色欲色欲www| 亚洲精品老司机在线观看| 亚欧日韩毛片在线看免费网站| 亚洲二区在线视频| 日韩精品久久久久久免费| 亚洲人成网站在线观看播放动漫 | 亚洲毛片免费观看| 国产在线ts人妖免费视频| 好男人资源在线WWW免费| 亚洲六月丁香六月婷婷蜜芽| 免费中文字幕在线观看| 久久免费国产视频| 国产产在线精品亚洲AAVV| 午夜亚洲国产理论秋霞| 大陆一级毛片免费视频观看i|