分类:
2008-10-28 18:10:52
我们都知道应该使用 finally
来释放像数据库连接这样的重量级对象,但是我们并不总是这样细心,能够记得使用它来关闭流(毕竟,终结器会为我们做这件事,是不是?)。很容易忘记在使用资源的代码不抛出已检查的异常时使用 finally
。清单 5 展示了针对绑定连接的 add()
方法的实现,它使用 Semaphore
来实施绑定,并有效地允许客户机等待空间可用:
|
LeakyBoundedSet
首先等待一个许可证成为可用的(表示连接中有空间了),然后试图将元素添加到连接中。添加操作如果由于该元素已经在连接中了而失败,那么它会释放许可证(因为它不实际使用它所保留的空间)。
与 LeakyBoundedSet
有关的问题没有必要马上跳出:如果 Set.add()
抛出一个异常呢?如果 Set
实现中有缺陷,或者 equals()
或 hashCode()
实现(在 SortedSet
的情况下是 compareTo()
实现)中有缺陷,原因在于添加元素时元素已经在 Set
中了。当然,解决方案是使用 finally
来释放信号量许可证,这是一个很简单却容易被遗忘的方法。这些类型的错误很少会在测试期间暴露出来,因而成了定时炸弹,随时可能爆炸。清单 6 展示了 BoundedSet
的一个更加可靠的实现:
|
像 FindBugs这样的代码审查工具可以检测出不适当的资源释放的一些实例,比如在一个方法中打开一个流却不关闭它。
对于具有任意生命周期的资源,我们要回到 C 语言的时代,即手动地管理资源生命周期。在一个服务器应用程序中,客户机到服务器的一个持久网络连接存在于一个会话期间(比如一个多人参与的游戏服务器),每个用户的资源(包括套接字连接)在用户退出时必须被释放。好的组织是有帮助的;如果对每个用户资源的角色引用保存在一个 ActiveUser 对象中,那么它们就可以在 ActiveUser 被释放时(无论是显式地释放,还是通过垃圾收集而释放)而被释放。
具有任意生命周期的资源几乎总是在一个全局集合中(或者从这里可达)。要避免资源泄漏,因此非常重要的是,要识别出资源何时不再需要了并可以从这个全局集合中删除了。(以前的一篇文章 “用弱引用堵住内存泄漏” 给出了一些有用的技巧。)此时,因为您知道资源将要被释放,任何与该资源关联的非内存资源也可以同时被释放。
确保及时的资源释放的一个关键技巧是维护所有权的一个严格层次结构,其中的所有权具有释放资源的职责。如果应用程序创建一个线程池,而线程池创建线程,线程是程序可以退出之前必须被释放的资源。但是应用程序不拥有线程,而是由线程池拥有线程,因此线程池必须负责释放线程。当然,直到它本身被应用程序释放之后,线程池才能释放线程。
维护一个所有权层次结构有助于不至于失去控制,其中每个资源拥有它获得的资源并负责释放它们。这个规则的结果是,每个不能由垃圾收集单独收集的资源(即这样的资源,它直接或间接拥有不能由垃圾收集释放的资源)必须提供某种生命周期支持,比如 close()
方法。
如果说平台库提供终结器来清除打开的文件句柄,这大大降低了忘记显式地关闭这些句柄的风险,为什么不更多地使用终结器呢?原因有很多,最重要的一个原因是,终结器很难正确编写(并且很容易编写错)。终结器不仅难以编写正确,终结的定时也是不确定的,并且不能保证终结器最终会运行。并且终结还为可终结对象的实例化和垃圾收集带来了开销。不要依赖于终结器作为释放资源的主要方式。
垃圾收集为我们做了大量可怕的资源清除工作,但是有些资源仍然需要显式的释放,比如文件句柄、套接字句柄、线程、数据库连接和信号量许可证。当资源的生命周期被绑定到特定调用帧的生命周期时,我们通常可以使用 finally
块来释放该资源,但是长期存活的资源需要一种策略来确保它们最终被释放。对于任何一个这样的对象,即它直接或间接拥有一个需要显式释放的对象,您必须提供生命周期方法 —— 比如 close()
、release()
、destroy()
等 —— 来确保可靠的清除。
[2]