被動實例化

被動實例化

                                                -- 性能與資源佔用之間的平衡

  譯者注:
       本來沒打算翻譯這篇文章,但前段時間進行代碼走查和bug Fix工作的時候,發現程  序代碼中依然存在這樣的問題.於是我就將這篇文章翻譯了出來.供大家參考,其中翻譯失誤的地方還請大家指教.謝謝!

   概要:
       自從計算機誕生以來,爲了避免浪費諸如內存這些有用的資源,軟件性能和資源消耗之間的平衡一直就是個問題,可能有時候還會犧牲一些軟件性能來達到這個目的。本專題探討在Java編程中通過將對象的創建推遲到系統真正需要時刻完成來降低內存消耗的一種方法途徑。(2400字數)

       當8位計算機主板上的內存從8KB一下子到64KB,我們爲之而激動不已的時刻似乎就發生在不久前,回頭看到我們現在使用的資源消耗如此之多而且還在不斷增加的應用系統,在過去能夠寫出適應那少的可憐內存的系統程序確實讓人驚訝。儘管我們現在有更多的內存資源可以使用,但從那些建立在以往諸多約束基礎上的技術中,還是能夠學到不少很有價值的經驗。

       此外,Java編程不僅僅是書寫一些部署到個人PC或工作站上的applet和應用程序.它已經深入涉足嵌入式系統的市場領域。現在的嵌入式系統的內存資源和計算能力仍然很有限.所以,很多以往面臨的老問題就又重新出現在了涉足嵌入式設備領域的Java程序員面前。

       平衡資源與性能這些因素是一個很讓人興奮的設計話題:在嵌入式系統的設計開發中,沒有任何一個解決方案是完美的。我們必須接受這個事實。所以,我們需要理解一些技術類型,也就是在現有的部署平臺約束下,對較好的達到我們上面提到的平衡有用的技術。

       其中,一個有效的避免內存消耗的技術就是被動實例化,Java程序員們在實際工作中發現很有用處。利用被動實例化技術,程序會在某些資源第一次被使用的時候纔去創建他。---這樣做的同時就相當於釋放出了有用的內存空間。在這個專題中,我們在類的裝載和對象的創建兩個方面探討被動實例化的技術,並且對於單例模式的情況我們做了特殊考慮。這些資料來源於我的著作 Java實戰: Design Styles & Idioms for Effective Java 中的第九個章節.

 主動初始化與被動初始化:一個例子

       如果你熟悉Netscapse網頁瀏覽器並且曾經用過3.x 和4.x版。那麼你肯定注意到了Java運行時機制在裝載時的不同之處。在Netscape 3.0啓動的時候,如果你注意閃動的屏幕,你就會發現他正在裝載各種資源,其中包括Java.然而,在Netscape 4.X中,瀏覽器並沒有裝載Java運行時機制---直到你運行了包括<APPLET>標籤的web頁面後它纔會被裝載。 這兩種解決方式體現出了主動實例化(裝載它,以備萬一)和被動實例化(直到他被請求才會裝載,或者它根本不會用到)技術。

      對於這兩種解決方案都存在一些缺點。一方面,經常地裝載一種資源會潛在地浪費寶貴的內存資源,如果這些被裝載的資源在會話期間根本不會用到。另一方面,對於被動初始化,如果先前資源沒有被裝載,在資源被第一次請求的時候,你要付出一定的裝載時間。

  將被動實例化作爲一種資源保持策略
        被動實例化在Java中可以分爲兩類。
           。類被動式裝載
           。對象被動式創建

           類被動式裝載
       
           Java 運行時機制已經內嵌了類的被動實例化操作。只有當類第一次被引用的 時候纔會裝載到內存中(同樣也可以通過HTTP 從web服務器裝載)
         
          MyUtils.classMethod();   //first call to a static class method
          Vector v = new Vector(); //first call to operator new
         
          類的被動裝載是一個JAVA運行環境中很重要的功能,在某種情況下,它降低了內存的使用。在一個會話期內如果一部分程序從來都沒有被執行,那麼在那段程序中被引用到的類將不會把被裝載。
 
          對象被動式創建
         
         對象的被動式創建和類的被動式裝載是緊密聯繫的。在一個從未裝載過的類型別上第一次使用New 關鍵字,Java運行時機制會爲你裝載它。對象的被動式創建相對於類的被動裝載能夠更大程度上降低內存的消耗.


         爲了介紹對象被動式創建這個概念,讓我門看一段簡單的代碼例子。這個例子主要是一個Frame使用MessageBox顯示錯誤信息。
         
          public class MyFrame extends Frame
          {
            private MessageBox mb_ = new MessageBox();
            //private helper used by this class
             private void showMessage(String message)
               {
              //set the message text
              mb_.setMessage( message );
              mb_.pack();
              mb_.show();
              }
           }


        在上面這個例子中,當一個MyFrame實例被創建時,MessageBox的實例對象 mb_也會被創建。如果被創建的對象中還有對象需要創建,那麼就會出現對象的遞歸創建。任何被實例化的或被分配到MessageBox的構造子中變量,都會在系統的heap中分配空間。如果在一個會話期中,MyFrame的實例沒有被用來去顯示錯誤信息的話,我們就浪費了沒必要的內存。
 
        在這個相當簡單的例子中,我們並不能發現被動對象創建有多麼明顯的優點,可是如果是一個更加複雜的類,在這個類中又使用了許多其他的類。這時候,你可能需要遞歸的實例化這些類。那麼這些潛在的內存消耗是相當明顯的。

       接下來,請看前面提到的例子的被動實現方法,在這個方法中,對象mb_在第一次調用showMessage()方法時被實例化(也就是說,直到它真正被程序需要的時候。)
 
  public final class MyFrame extends Frame
 
  {
  private MessageBox mb_ ; //null, implicit
  //private helper used by this class
  private void showMessage(String message)
  {
    if(mb_==null)//first call to this method
      mb_=new MessageBox();
    //set the message text
    mb_.setMessage( message );
    mb_.pack();
    mb_.show();
  }
}

       如果你仔細觀察showMessage()方法,你會看到我們首先檢測是否實例變量mb_等於Null.對於變量mb_在聲明的時候沒有對它進行進行實例化的情況,Java 運行時機制已經替我們處理了(譯者注:會將引用類型的變量設爲Null).因此,我們可以通過創建MessageBox實例對象,繼續安全地執行程序.對於以後所有對showMessage()方法的調用都將會發現實例變量mb_不等於Null,因此會跳過對象創建這一步直接使用現有的實例。

一個真實世界的例子

       現在,讓我們來看一個更加貼近實際的例子,在這個例子中,被動實例化在降低程序所使用的資源方面扮演了一個很重要的角色。

      假設我們應顧客的要求需要寫一個系統,這個系統能夠讓用戶分類文件系統中的圖片並且能夠提供一個工具用來瀏覽縮略圖或全圖。那麼我們首先想到的實現方式就是寫一個類在它的構造子中來裝載圖片。

public class ImageFile
{
  private String filename_;
  private Image image_;
  public ImageFile(String filename)
  {
    filename_=filename;
    //load the image
  }
  public String getName(){ return filename_;}
  public Image getImage()
  {
    return image_;
  }
}

       在上面的例子中,ImageFile在實例化Image對象時採用了積極的解決途徑。這種設計方式保證了getImage()方法調用的時候image對象立即生效,以供使用。但是,這種實現方式不但在速度上會慢的讓人感到痛苦(文件夾中存在很多圖片的情況下),而且這種設計會消耗太多可用內存。爲了避免這個潛在的問題,降低內存消耗,我們放棄了這種瞬間訪問方式所帶來的性能益處.你可能早就猜到了,我們可以使用被動實例化來達到我們的要求.

      下面是改動後的ImageFile類,它使用了和MyFrame類中的MessageBox實例變量一樣的解決方法。

public class ImageFile
{
  private String filename_;
  private Image image_; //=null, implicit
  public ImageFile(String filename)
  {
    //only store the filename
    filename_=filename;
  }
  public String getName(){ return filename_;}
  public Image getImage()
  {

    if(image_==null)
    {
      //first call to getImage()
      //load the image...
    }
    return image_;
  }
}

       在這個版本中,只有在getImage()方法調用的時候,image_實例變量才被裝載。這樣改動的目的就是降低整體內存的使用量和裝載動作的啓動次數,同時我們爲之付出的代價就是在第一次請求的時候裝載它.這是被動實例化的另一特性--在內存使用受到約束的上下文環境中表現代理模式(Proxy pattern).


     上面展示出的被動初始化策略對於我們這個例子已經夠了。但馬上,你會看到這樣的設計如果用在多線程的上下文環境中就需要更改了。

單例模式下的被動實例化

讓我們來看一個單例模式的例子。下面就是這種模式在Java中的一般表現手法。

{
  private Singleton() {}
  static private Singleton instance_
    = new Singleton();
  static public Singleton instance()
  {
    return instance_;
  }
  //public methods
}

      在這個較普遍的單例模式版本,我們這樣聲明並初始化了instance_域成員:
static final Singleton instance_ = new Singleton();

       比較熟悉"四人幫"(Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著 Design Patterns: Elements of Reusable Object-Oriented Software一書)著作中所描述的C++單例模式的實現方式的讀者可能會對我們沒有將變量instance_的初始化動作推遲到instance()方法調用的時候發生而感到驚訝。因此,他們會認爲應該使用被動實例化,象這樣:

public static Singleton instance()
{

  if(instance_==null) //Lazy instantiation

    instance_= new Singleton();
  return instance_;
}

       上面列出的代碼就是原封不動從"四人幫"的C++單例模式實例中拿過來的,而且這種方式也經常被說成是Java程序的典範。如果你已經對這種方式比較熟悉並且吃驚於我並沒有以同樣的方式列出Java的單例模式,你甚至於會認爲在Java中我那樣做完全沒有必要。一旦你對各自語言的運行期環境差別不加思索就直接將代碼從一種語言移植到另一個語言,那麼出現上面的迷惑是很常見的。 }

      確切點說,"四人幫"的C++單例模式使用了被動初始化是因爲在運行期無法保證對象的靜態初始化的順序。(請參考Scott Meyer的單例模式在C++中的替代實現方式)在Java中,我們完全不用擔心這一點。

       由於Java運行期對於類裝載和靜態static實例變量的初始化的處理方式(譯者注:類裝載時刻類中的靜態成員先於非靜態成員變量被自動初始化而且只能被初始化一次),在單例模式中被動初始化的解決方式在JAVA中是沒有必要的.先前,我們提到過類是什麼時候以什麼方式被裝載。在運行時,一個只有public static方法的類是當他的方法被第一次調用的時候被裝載的。就象這樣

Singleton s=Singleton.instance();

       在程序中第一次調用Singleton.instance()將迫使Java運行時去裝載類Singleton。由於instance_ 變量是被聲明爲靜態static.Java運行時會在成功裝載類之後對這個變量進行初始化。因此保證了Singleton.instance()方法能夠返回一個完全初始化的單例,不知道你理解了嗎?


被動初始化在多線程時的危險性
  
      在Java的一個具體的單例模式實現中使用被動實例化不僅是沒有必要的,而且這樣一來還會造成在多線程應用程序的上下文中危險性。讓我們來考慮一下Singleton.instance()方法的被動實例化的實現方式,如果有兩個或更多的獨立線程正在試圖通過instance()方法獲取對象的引用。如果其中一個線程搶先一步執行了if(instance_==null)這個判斷語句,但在她執行instance_=new Singleton()語句之前,另一個線程也已經進入了這個方法併成功執行了if(instance_==null)的判斷,這時候就會出現髒數據現象。

       象我們剛纔這個假設一旦出現,所帶來的後果就是一個或更多的singleton對象被創建。假設,如果你的Singleton 類負責的是連接數據庫或遠程服務器的話,那可就真成了麻煩事了。這個問題一個簡單的解決方式就是使用synchronized關鍵字來防止在多線程的情況下同時訪問一個方法。

    synchronized static public instance() {...}
   
       但是,這樣一來在廣泛實現了單例模式的多線程程序中會存在問題,它將會阻止對instance() 方法的同步訪問。而且調用一個同步方法通常比調用一個非同步的方法要慢。我們真正需要的同步策略是不該造成不必要的阻塞的.幸運的是,我們存在這樣的解決方法。那就是我們所說的 "雙重訪問模式"(double-check idiom)

   雙重訪問模式
  
   使用雙重訪問模式來保護使用了被動實例化的方法,下面就是我們的具體實現方法
 
   public static Singleton instance()
{

  if(instance_==null) //don't want to block here

  {

    //two or more threads might be here!!!
    synchronized(Singleton.class)

    {

      //must check again as one of the

      //blocked threads can still enter
      if(instance_==null)

        instance_= new Singleton();//safe

    }

  }

  return instance_;
}


       在Singleton被構建之前,多個線程調用instance()方法時,"雙重訪問"方法可以通過同步來改善性能.一旦對象被實例化後,instance_變量就不再等於null了,同時也避免了阻塞其它同步調用者.

       在Java中使用多線程會使系統變的很複雜。其實,同步這個課題是很廣泛的,Doug Lea已經專門就它著作了一本書:Concurrent Programming in Java。如果你現在正在涉足同步編程。那麼我建議在你開始書寫依賴多線程的複雜Java系統之前,你最好看一下這本書.

  總結:
 
       通常我們經常制定一些協議來滿足客戶的需求與期望或者更有效地利用有限的資源。理解Java中實現被動實例化的技術,再加上Java語言提供的豐富功能和運行時環境(JRE),可以幫助你設計出高效的,優美的,快速的軟件系統。關於主動實例化還有主動和被動實現的深入調查評估資料可以到我們的著作裏查到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章