被动实例化 -- 性能与资源占用之间的平衡
2004年翻译的一篇文章,现在看还是有些帮助,贴出来大家一起看看。
=======================================================
译者注:
本来没打算翻译这篇文章,但前段时间进行代码走查和bug Fix工作的时候,发现程 序代码中依然存在这样的问题.于是我就将这篇文章翻译了出来.供大家参考,其中翻译失误的地方还请大家指教.谢谢!
概要:
自从计算机诞生以来,为了避免浪费诸如内存这些有用的资源,软件性能和资源消耗之间的平衡一直就是个问题,可能有时候还会牺牲一些软件性能来达到这个目的。本专题探讨在Java编程中通过将对象的创建推迟到系统真正需要时刻完成来降低内存消耗的一种方法途径。
当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页面后它才会被装载。 这两种解决方式体现出了主动实例化(装载它,以备万一)和被动实例化(直到他被请求才会装载,或者它根本不会用到)技术。
对于这两种解决方案都存在一些缺点。一方面,经常地装载一种资源会潜在地浪费宝贵的内存资源,如果这些被装载的资源在会话期间根本不会用到。另一方面,对于被动初始化,如果先前资源没有被装载,在资源被第一次请求的时候,你要付出一定的装载时间
将被动实例化作为一种资源保持策略
- 类被动式装载
- 对象被动式创建
类被动式装载
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).
上面展示出的被动初始化策略对于我们这个例子已经够了。但马上,你会看到这样的设计如果用在多线程的上下文环境中就需要更改了。
单例模式下的被动实例化
{ 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() {...}
双重访问模式
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系统之前,你最好看一下这本书.
总结