我正在尝试追踪我们系统中的问题,以下代码让我担心.主servlet中的doPost()方法发生以下情况(名称已更改以保护有罪):
... if(Single.getInstance().firstTime()){ doPreperations(); } normalResponse(); ...
单身'单身'看起来像这样:
private static Single theInstance = new Single(); private Single() { ...load properties... } public static Single getInstance() { return theInstance; }
通过这种方式设置为使用静态初始化程序而不是在getInstance()方法中检查null theInstance,是否可以一次又一次地重建?
PS - 我们正在使用Java 1.4上的App运行WebSphere 6
我在Sun的网站上发现了这个:
不同类装载机同时装载多个单身人士
当两个类加载器加载一个类时,实际上有两个类的副本,每个类都可以拥有自己的Singleton实例.这在某些servlet引擎(例如iPlanet)中运行的servlet中尤为重要,其中每个servlet默认使用自己的类加载器.实际上,访问联合Singleton的两个不同的servlet将获得两个不同的对象.
多个类加载器比您想象的更常见.当浏览器从网络加载类以供applet使用时,它们为每个服务器地址使用单独的类加载器.类似地,Jini和RMI系统可以为它们下载类文件的不同代码库使用单独的类加载器.如果您自己的系统使用自定义类加载器,则可能会出现所有相同的问题.
如果由不同的类加载器加载,则具有相同名称的两个类(甚至相同的包名称)将被视为不同 - 即使实际上它们是逐字节的同一个类.不同的类加载器表示区分类的不同名称空间(即使类的名称相同),因此这两个
MySingleton
类实际上是不同的.(请参阅参考资料中的"类加载器作为命名空间机制".)由于两个Singleton对象属于同一名称的两个类,因此乍一看它会出现两个同一类的Singleton对象.
引用.
除了上述问题,如果firstTime()
不同步,您也可能遇到线程问题.
不,它不会一次又一次地建造.它是静态的,因此只有在类加载器第一次触摸类时才会构造一次.
唯一的例外 - 如果您碰巧有多个类加载器.
(来自GeekAndPoke):
正如其他人所提到的,静态初始化程序只能为每个类加载器运行一次.
我要看一看的是firstTime()
方法 - 为什么不能doPreparations()
在单例本身内处理工作?
听起来像一堆讨厌的依赖.
使用静态初始化程序和延迟初始化之间绝对没有区别.事实上,搞乱延迟初始化要容易得多,这也会强制实现同步.JVM保证静态初始化程序始终在访问类之前运行,并且只会发生一次.
这就是说JVM不保证你的类只加载一次.但是,即使它被加载多次,您的Web应用程序仍将只看到相关的单例,因为它将加载到Web应用程序类加载器或其父级中.如果部署了多个Web应用程序,则将为每个应用程序调用firstTime()一次.
要检查的最明显的事情是firstTime()需要同步,并且在退出该方法之前设置firstTime标志.
不,它不会创建"单个"的多个副本.(稍后将访问类加载器问题)
您概述的实现在Briant Goetz的书" Java Concurrency in Practice " 中被描述为"Eager Initialization ".
public class Single { private static Single theInstance = new Single(); private Single() { // load properties } public static Single getInstance() { return theInstance; } }
但是,代码不是你想要的.您的代码在创建实例后尝试执行延迟初始化.这要求所有客户端库在使用之前执行'firstTime()/ doPreparation()'.您将依赖客户端做正确的事情,使代码非常脆弱.
您可以按如下所示修改代码,这样就不会有任何重复的代码.
public class Single { private static Single theInstance = new Single(); private Single() { // load properties } public static Single getInstance() { // check for initialization of theInstance if ( theInstance.firstTime() ) theInstance.doPreparation(); return theInstance; } }
不幸的是,这是一个很糟糕的初始化实现,这在并发环境(如J2EE容器)中不起作用.
有很多关于Singleton初始化的文章,特别是关于内存模型的文章. JSR 133解决了Java 1.5和1.6中Java内存模型的许多弱点.
使用Java 1.5和1.6,您有几种选择,Joshua Bloch 在" Effective Java " 一书中提到了它们.
渴望初始化,如上所述[EJ第3项]
Lazy Initalization Holder Class Idiom [EJ Item 71]
枚举类型[EJ第3项]
带有"易变"静态区域的双重检查锁定[EJ Item 71]
解决方案3和4仅适用于Java 1.5及更高版本.所以最好的解决方案是#2.
这是伪实现.
public class Single { private static class SingleHolder { public static Single theInstance = new Single(); } private Single() { // load properties doPreparation(); } public static Single getInstance() { return SingleHolder.theInstance; } }
请注意,'doPreparation()'在构造函数内部,因此您可以保证获得正确初始化的实例.此外,您正在回顾JVM的延迟类加载,并且不需要任何同步'getInstance()'.
有一点你注意到静态字段theInstance 不是'final'. 关于Java Concurrency的示例没有'final',但EJ没有.也许詹姆斯可以为他的"类加载器"和"最终"要求添加更多颜色以保证正确性,
话虽如此,使用'静态最终'会产生副作用.当Java编译器看到'static final'并试图尽可能地内联它时,它是非常积极的.这是Jeremy Manson在博客上发表的.
这是一个简单的例子.
档案:A.java
public class A { final static String word = "Hello World"; }
档案:B.java
public class B { public static void main(String[] args) { System.out.println(A.word); } }
在编译A.java和B.java之后,将A.java更改为以下内容.
档案:A.java
public class A { final static String word = "Goodbye World"; }
您重新编译'A.java'并重新运行B.class.你会得到的输出是
Hello World
至于类加载器问题,答案是肯定的,你可以在多个类加载器中有多个Singleton实例.您可以在维基百科上找到更多信息.Websphere上还有一篇特定的文章.