程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java 理論與實踐: 良好的內務處理實踐

Java 理論與實踐: 良好的內務處理實踐

編輯:關於JAVA
垃圾收集幾乎是每個開發人員都喜愛的一個 Java 平台特性,它簡化了開發,消除了所有種類的潛在代碼錯誤。可盡管垃圾收集一般來說可以讓您無需進行資源管理,有時候您還是必須自己進行一些內務處理。在本文中,Brian Goetz 討論了垃圾收集的局限性,並指出了您必須自己做內務處理的場景。

  小時候,父母總是叮囑我們玩了玩具之後要收好。如果您仔細想想,其實這種唠叨並不過分,要保持整潔是因為存在實際的限制,房間裡沒有太多的空間,如果到處堆滿了玩具,那麼連走路都無處下腳了。 

  如果有了足夠的空間,保持整潔就不是那麼必要了。空間越多,就越不必要保持整潔。Arlo GuthrIE 著名的民謠 Alice's Restaurant Massacre 說明了這一點:

  他們住在教堂樓下的大廳,裡面的椅子全都搬走了,剩下一個空蕩蕩的大房間,所以他們想,很長時間都不用把垃圾扔出去,有的是地方裝垃圾…… 

  無論如何,垃圾收集可以幫我們減輕內務整理方面的工作。 

  顯式地釋放資源

  Java 程序中使用的絕大多數資源都是對象,垃圾收集在清理對象方面做得很好。因此,您可以使用任意多的 String。垃圾收集器最終無需您的干預就會算出它們何時失效,並收回它們使用的內存。 

  另一方面,像文件句柄和套接字句柄這類非內存資源必須由程序顯式地釋放,比如使用 close()、destroy()、shutdown() 或 release() 這樣的方法來釋放。有些類,比如平台類庫中的文件句柄流實現,提供終結器(finalizer)作為安全保證,以便當垃圾收集器確定程序不再使用資源而程序卻忘了釋放資源時,終結器還可以來做這個釋放工作。但是盡管文件句柄提供了終結器來在您忘記了時為您釋放資源,最好還是在使用完之後顯式地釋放資源。這樣做可以更早地釋放資源,降低了資源耗盡的可能。 

  對於有些資源來說,一直等到終結(finalization)釋放它們是不可取的。對於重要的資源,比如鎖獲取和信號量許可證,Lock 或 Semaphore 直到很晚都可能不會被垃圾收集掉。對於數據庫連接這樣的資源,如果您等待終結,那麼肯定會消耗完資源。許多數據庫服務器根據許可的容量,只接受一定數量的連接。如果服務器應用程序為每個請求都打開一個新的數據庫連接,然後用完之後就不管了,那麼數據庫遠遠未到終結器關閉不再需要的連接,就會到達它的最高容量。 

  只限於一個方法的資源

  多數資源都不會持續整個應用程序的生命周期,相反,它們只被用於一個活動的生命周期。當應用程序打開一個文件句柄讀取文件以處理文檔時,它通常讀取文件後就不再需要文件句柄了。

  在最簡單的情況下,資源在同一個方法調用中被獲取、使用和釋放,比如清單 1 中的 loadPropertIEsBadly() 方法: 

  清單 1. 不正確地在一個方法中獲取、使用和釋放資源 —— 不要這樣做

public static Properties loadPropertIEsBadly(String fileName)
throws IOException {
 FileInputStream stream = new FileInputStream(fileName);
 Properties props = new PropertIEs();
 props.load(stream);
 stream.close();
 return props;
}
  不幸的是,這個例子存在潛在的資源洩漏。如果一切進展順利,流將會在方法返回之前被關閉。但是如果 props.load() 方法拋出一個 IOException,那麼流則不會被關閉(直到垃圾收集器運行其終結器)。解決方案是使用 try...finally 機制來確保流被關閉,而不管是否發生錯誤,如清單 2 所示:

  清單 2. 正確地在一個方法中獲取、使用和釋放資源

public static Properties loadPropertIEs(String fileName) 
throws IOException {
 FileInputStream stream = new FileInputStream(fileName);
 try {
  Properties props = new PropertIEs();
  props.load(stream);
  return props;
 }
 finally {
  stream.close();
 }
}
  注意,資源獲取(打開文件)是在 try 塊外面進行的;如果把它放在 try 塊中,那麼即使資源獲取拋出異常,finally 塊也會運行。不僅該方法會不適當(您無法釋放您沒有獲取的資源),finally 塊中的代碼也可能拋出其自己的異常,比如 NullPointerException。從 finally 塊拋出的異常取代導致塊退出的異常,這意味著原來的異常丟失了,不能用於幫助進行調試。 並不總像看起來那麼容易

  使用 finally 來釋放在方法中獲取的資源是可靠的,但是當涉及多個資源時,很容易變得難以處理。下面考慮這樣一個方法,它使用一個 JDBC Connection 來執行查詢和迭代 ResultSet。該方法獲得一個 Connection,使用它來創建一個 Statement,並執行 Statement 以得到一個 ResultSet。但是中間 JDBC 對象 Statement 和 ResultSet 具有它們自己的 close() 方法,並且當您使用完之後,應該釋放這些中間對象。然而,進行資源釋放的 “明顯的” 方式並不起作用,如清單 3 所示: 

  清單 3. 不成功的釋放多個資源的企圖 —— 不要這樣做

public void enumerateFoo() throws SQLException {
 Statement statement = null;
 ResultSet resultSet = null;
 Connection connection = getConnection();
 try {
  statement = connection.createStatement();
  resultSet = statement.executeQuery("SELECT * FROM Foo");
  // Use resultSet
 }
 finally {
  if (resultSet != null)
   resultSet.close();
  if (statement != null)
   statement.close();
  connection.close();
 }
}
  這個 “解決方案” 不成功的原因在於,ResultSet 和 Statement 的 close() 方法自己可以拋出 SQLException,這會導致後面 finally 塊中的 close() 語句不執行。您在這裡有幾種選擇,每一種都很煩人:用一個 try..catch 塊封裝每一個 close(),像清單 4 那樣嵌套 try...finally 塊,或者編寫某種小型框架用於管理資源獲取和釋放。 

  清單 4. 可靠的釋放多個資源的方法

public void enumerateBar() throws SQLException {
 Statement statement = null;
 ResultSet resultSet = null;
 Connection connection = getConnection();
 try {
  statement = connection.createStatement();
  resultSet = statement.executeQuery("SELECT * FROM Bar");
  // Use resultSet
 }
 finally {
  try {
   if (resultSet != null)
    resultSet.close();
  }
  finally {
   try {
    if (statement != null)
     statement.close();
   }
   finally {
    connection.close();
   }
  }
 }
}

private Connection getConnection() {
 return null;
}
  幾乎每一樣東西都可以拋出異常

  我們都知道應該使用 finally 來釋放像數據庫連接這樣的重量級對象,但是我們並不總是這樣細心,能夠記得使用它來關閉流(畢竟,終結器會為我們做這件事,是不是?)。很容易忘記在使用資源的代碼不拋出已檢查的異常時使用 finally。清單 5 展示了針對綁定連接的 add() 方法的實現,它使用 Semaphore 來實施綁定,並有效地允許客戶機等待空間可用:

  清單 5. 綁定連接的脆弱實現 —— 不要這樣做

public class LeakyBoundedSet<T> {
 private final Set<T> set = ...
 private final Semaphore sem;

 public LeakyBoundedSet(int bound) {
  sem = new Semaphore(bound);
 }

 public boolean add(T o) throws InterruptedException {
  sem.acquire();
  boolean wasAdded = set.add(o);
  if (!wasAdded)
   sem.release();
  return wasAdded;
 }
}
  LeakyBoundedSet 首先等待一個許可證成為可用的(表示連接中有空間了),然後試圖將元素添加到連接中。添加操作如果由於該元素已經在連接中了而失敗,那麼它會釋放許可證(因為它不實際使用它所保留的空間)。 

  與 LeakyBoundedSet 有關的問題沒有必要馬上跳出:如果 Set.add() 拋出一個異常呢?如果 Set 實現中有缺陷,或者 equals() 或 hashCode() 實現(在 SortedSet 的情況下是 compareTo() 實現)中有缺陷,原因在於添加元素時元素已經在 Set 中了。當然,解決方案是使用 finally 來釋放信號量許可證,這是一個很簡單卻容易被遺忘的方法。這些類型的錯誤很少會在測試期間暴露出來,因而成了定時炸彈,隨時可能爆炸。清單 6 展示了 BoundedSet 的一個更加可靠的實現: 

  清單 6. 使用一個 Semaphore 來可靠地綁定 Set

public class BoundedSet<T> {
 private final Set<T> set = ...
 private final Semaphore sem;

 public BoundedHashSet(int bound) {
  sem = new Semaphore(bound);
 }

 public boolean add(T o) throws InterruptedException {
  sem.acquire();
  boolean wasAdded = false;
  try {
   wasAdded = set.add(o);
   return wasAdded;
  }
  finally {
   if (!wasAdded)
    sem.release();
  }
 }
}
  像 FindBugs這樣的代碼審查工具可以檢測出不適當的資源釋放的一些實例,比如在一個方法中打開一個流卻不關閉它。 

  具有任意生命周期的資源

  對於具有任意生命周期的資源,我們要回到 C 語言的時代,即手動地管理資源生命周期。在一個服務器應用程序中,客戶機到服務器的一個持久網絡連接存在於一個會話期間(比如一個多人參與的游戲服務器),每個用戶的資源(包括套接字連接)在用戶退出時必須被釋放。好的組織是有幫助的;如果對每個用戶資源的角色引用保存在一個 ActiveUser 對象中,那麼它們就可以在 ActiveUser 被釋放時(無論是顯式地釋放,還是通過垃圾收集而釋放)而被釋放。 

  具有任意生命周期的資源幾乎總是存儲在一個全局集合中(或者從這裡可達)。要避免資源洩漏,因此非常重要的是,要識別出資源何時不再需要了並可以從這個全局集合中刪除了。(以前的一篇文章 “用弱引用堵住內存洩漏” 給出了一些有用的技巧。)此時,因為您知道資源將要被釋放,任何與該資源關聯的非內存資源也可以同時被釋放。

  資源所有權

  確保及時的資源釋放的一個關鍵技巧是維護所有權的一個嚴格層次結構,其中的所有權具有釋放資源的職責。如果應用程序創建一個線程池,而線程池創建線程,線程是程序可以退出之前必須被釋放的資源。但是應用程序不擁有線程,而是由線程池擁有線程,因此線程池必須負責釋放線程。當然,直到它本身被應用程序釋放之後,線程池才能釋放線程。 

  維護一個所有權層次結構有助於不至於失去控制,其中每個資源擁有它獲得的資源並負責釋放它們。這個規則的結果是,每個不能由垃圾收集單獨收集的資源(即這樣的資源,它直接或間接擁有不能由垃圾收集釋放的資源)必須提供某種生命周期支持,比如 close() 方法。 

  終結器

  如果說平台庫提供終結器來清除打開的文件句柄,這大大降低了忘記顯式地關閉這些句柄的風險,為什麼不更多地使用終結器呢?原因有很多,最重要的一個原因是,終結器很難正確編寫(並且很容易編寫錯)。終結器不僅難以編寫正確,終結的定時也是不確定的,並且不能保證終結器最終會運行。並且終結還為可終結對象的實例化和垃圾收集帶來了開銷。不要依賴於終結器作為釋放資源的主要方式。 

  結束語

  垃圾收集為我們做了大量可怕的資源清除工作,但是有些資源仍然需要顯式的釋放,比如文件句柄、套接字句柄、線程、數據庫連接和信號量許可證。當資源的生命周期被綁定到特定調用幀的生命周期時,我們通常可以使用 finally 塊來釋放該資源,但是長期存活的資源需要一種策略來確保它們最終被釋放。對於任何一個這樣的對象,即它直接或間接擁有一個需要顯式釋放的對象,您必須提供生命周期方法 —— 比如 close()、release()、destroy() 等 —— 來確保可靠的清除。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved