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() 等 —— 来确保可靠的清除。

时间: 2024-11-05 12:30:43

Java 理论与实践: 良好的内务处理实践的相关文章

Java 理论与实践:变还是不变?

不变对象具有许多能更方便地使用它们的特性,包括不严格的同步需求和不必考虑数据讹误就能自由地共享和高速缓存对象引用.尽管不变性可能未必对于所有类都有意义,但大多数程序中至少有一些类将受益于不可变.在本月的 Java 理论与实践中,Brian Goetz 说明了不变性的一些长处和构造不变类的一些准则.请在附带的论坛中与作者和其他读者分享您关于本文的心得.(也可以单击文章顶部或底部的"讨论"来访问论坛.) 不变对象是指在实例化后其外部可见状态无法更改的对象.Java 类库中的 String.

Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制

伸缩 内容: synchronized 快速回顾 对 synchronized 的改进 比较 ReentrantLock 和 synchronized 的可伸缩性 条件变量 这不公平 结束语 参考资料 关于作者 对本文的评价 相关内容: Java 理论与实践 系列 Synchronization is not the enemy Reducing contention IBM developer kits for the Java platform (downloads) 订阅: develop

Java理论与实践: 您的小数点到哪里去了?

许多程序员在其整个开发生涯中都不曾使用定点或浮点数,可能的例外是, 偶尔在计时测试或基准测试程序中会用到.Java语言和类库支持两类非整数类型 ― IEEE 754 浮点( float 和 double ,包装类(wrapper class)为 Float 和 Double ),以及任意精度的小数( java.math.BigDecimal ).在本月的 Java 理论和实践中,Brian Goetz 探讨了在 Java 程序中使用非整数类型时一 些常碰到的陷阱和"gotcha". 虽

Java理论与实践: 垃圾收集简史

Java 语言可能是使用最广泛的依赖于垃圾收集的编程语言,但是它并不是第 一个.垃圾收集已经成为了包括 Lisp.Smalltalk.Eiffel.Haskell.ML. Scheme和 Modula-3 在内的许多编程语言的一个集成部分,并且从 20 世纪 60 年代早期就开始使用了.在 Java 理论与实践的本篇文章中,Brian Goetz 描述 了垃圾收集最常用的技术. 垃圾收集的好处是无可争辩的 ―― 可靠性提高.使内存管理与类接口设计 分离,并使开发者减少了跟踪内存管理错误的时间.著

Java理论与实践: 构建一个更好的HashMap

ConcurrentHashMap 是 Doug Lea的 util.concurrent 包的一部分,它提供 比Hashtable 或者 synchronizedMap 更高程度的并发性.而且,对于大多数成 功的 get() 操作它会设法避免完全锁定,其结果就是使得并发应用程序有着非 常好的吞吐量.这个月,BrianGoetz 仔细分析了 ConcurrentHashMap的代码, 并探讨 Doug Lea 是如何在不损失线程安全的情况下取得这么骄人成绩的. 在7月份的那期 Java理论与实践

Java理论与实践: 消除bug

很多有关编程风格的建议都是为了创建高质量.可维护的代码,这很合理, 因为最容易修复 bug 的时间就是在产生 bug 之前(少量的预防措施--).遗 憾的是,只预防往往是不够的,虽然有一些精巧的工具可以帮助您创建好的代码 ,但是很少有工具可以帮助您分析.维护或提高现有代码的质量. 写线程安全的类很难,而分析现有类的线程安全性更难,增强类使其仍然保 持线程安全也很难.以隐含假定.不变式以及预期用例(虽然在开发人员的头脑 中很清晰,但是没有以设计笔记.注释或者文档的方式记录下来)的方式编写完 类之后

Java理论与实践: 修复Java内存模型,第1部分

活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型 (Java Memory Model, JMM)的公开建议.原始 JMM 中有几个严重缺陷,这导 致了一些难度高得惊人的概念语义,这些概念原来被认为很简单,如 volatile .final 以及 synchronized.在这一期的 Java 理论与实践 中,Brian Goetz 展示了如何加强 volatile 和 final 的语义,以修复 JMM.这些更改有些已经 集成在 JDK 1.4 中:而另一些将会包含

Java理论与实践:做个好的(事件)侦听器

观察者模式在 Swing 开发中很常见,在 GUI 应用程序以外的场景中,它对 于消除组件的耦合性也非常有用.但是,仍然存在一些侦听器登记和调用方面的 常见缺陷.在 Java 理论与实践 的这一期中,Java 专家 Brian Goetz 就如何 做一个好的侦听器,以及如何对您的侦听器也友好,提供了一些感觉很好的建议 .请在相应的 讨论论坛 上与作者和其他读者分享您对这篇文章的想法.(您也 可以单击本文顶部或底部的 讨论 访问论坛.) Swing 框架以事件侦听器的形式广泛利用了观察者模式(也称

Java理论与实践专题

Java理论与实践: JDK 5.0中更灵活.更具可伸缩性的锁定机制 Java理论和实践: 一个有缺陷的微基准的剖析 Java理论和实践: 理解JTS ― 平衡安全性和性能 Java理论和实践: 理解JTS ― 幕后魔术 Java理论和实践: 安全构造技术 Java理论与实践: 平衡测试,第3部分:用方面检验设计约束 Java理论与实践:平衡测试,第2部分:编写和优化bug检测器 Java理论与实践:平衡测试,第1部分:不要仅编写测试,还要编写bu Java理论与实践: 您的小数点到哪里去了?