13

我试图找出我们系统中的一个问题,下面的代码让我很担心。以下发生在我们的主 servlet 中的 doPost() 方法中(名称已更改以保护有罪者):

...
if(Single.getInstance().firstTime()){
   doPreperations();
}
normalResponse();
...

单例“Single”如下所示:

private static Single theInstance = new Single();

private Single() {
...load properties...
}

public static Single getInstance() {
    return theInstance;
}

通过将其设置为使用静态初始化程序而不是在 getInstance() 方法中检查 null theInstance 的方式,这可以一遍又一遍地重建吗?

PS - 我们在 Java 1.4 上运行带有应用程序的 WebSphere 6

4

10 回答 10

16

我在 Sun 的网站上找到了这个:

不同的类加载器同时加载多个单例

当两个类加载器加载一个类时,您实际上有该类的两个副本,每个副本都可以有自己的 Singleton 实例。这与在某些 servlet 引擎(例如 iPlanet)中运行的 servlet 尤其相关,其中每个 servlet 默认使用自己的类加载器。事实上,访问联合 Singleton 的两个不同的 servlet 将获得两个不同的对象。

多个类加载器比您想象的更常见。当浏览器从网络加载类以供小程序使用时,它们为每个服务器地址使用单独的类加载器。类似地,Jini 和 RMI 系统可以为它们下载类文件的不同代码库使用单独的类加载器。如果您自己的系统使用自定义类加载器,则可能会出现所有相同的问题。

如果由不同的类加载器加载,两个具有相同名称的类,甚至相同的包名称,都被视为不同的——即使它们实际上是逐字节的同一个类。不同的类加载器代表不同的命名空间来区分类(即使类的名称相同),因此这两个 MySingleton类实际上是不同的。(请参阅参考资料中的“类加载器作为命名空间机制”。)由于两个 Singleton 对象属于同名的两个类,因此乍一看会出现同一个类的两个 Singleton 对象。

引文

除了上述问题之外,如果firstTime()未同步,您也可能在那里遇到线程问题。

于 2008-10-22T14:14:18.987 回答
13

不,它不会一遍又一遍地构建。它是静态的,所以它只会被构造一次,就在类加载器第一次接触该类时。

唯一的例外——如果你碰巧有多个类加载器。

(来自GeekAndPoke):

替代文字

于 2008-10-22T14:12:29.427 回答
5

正如其他人所提到的,每个类加载器只会运行一次静态初始化程序。

我要看的一件事是firstTime()方法 - 为什么不能在doPreparations()单例本身内处理工作?

听起来像是一组令人讨厌的依赖项。

于 2008-10-22T14:18:33.907 回答
5

使用静态初始化器和延迟初始化绝对没有区别。事实上,搞乱延迟初始化要容易得多,这也会强制同步。JVM 保证静态初始化程序总是在访问类之前运行,并且只会发生一次。

也就是说,JVM 不能保证你的类只会被加载一次。但是,即使它被多次加载,您的 Web 应用程序仍然只能看到相关的单例,因为它将在 Web 应用程序类加载器或其父级中加载。如果您部署了多个 Web 应用程序,那么 firstTime() 将为每个应用程序调用一次。

要检查的最明显的事情是 firstTime() 需要同步,并且在退出该方法之前设置了 firstTime 标志。

于 2008-10-22T14:44:20.300 回答
5

不,它不会创建“Single”的多个副本。(稍后会访问 Classloader 问题)

您概述的实现在 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 ”一书中提到。

  1. Eager Initialziation,如上[EJ Item 3]
  2. Lazy Initalization Holder 类成语 [EJ Item 71]
  3. 枚举类型 [EJ 项目 3]
  4. 带有“volatile”静态字段的双重检查锁定 [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不是“最终”的一件事。 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 实例。您可以在wikipedia上找到更多信息。还有一篇关于Websphere的具体文章。

于 2008-10-22T21:37:12.993 回答
2

关于单例实现(除了根本不使用单例),我唯一要改变的就是使实例字段成为最终的。静态字段将在类加载时初始化一次。由于类是延迟加载的,因此您可以有效地免费获得延迟实例化。

当然,如果它是从单独的类加载器加载的,您会得到多个“单例”,但这是 Java 中每个单例惯用语的限制。

编辑: firstTime() 和 doPreparations() 位看起来确实很可疑。不能将它们移到单例实例的构造函数中吗?

于 2008-10-22T14:17:24.270 回答
1

否 - 的静态初始化instance只会执行一次。需要考虑两件事:

  • 这不是线程安全的(实例未“发布”到主内存)
  • 除非正确同步,否则您的firstTime方法可能会被多次调用
于 2008-10-22T14:14:07.177 回答
1

理论上它只会被建造一次。但是,这种模式在各种应用程序服务器中中断,您可以获得“单例”类的多个实例(因为它们不是线程安全的)。

此外,单例模式也受到了很多批评。看例如Singleton 我爱你,但你让我失望

于 2008-10-22T14:16:18.670 回答
0

这只会在类加载器加载类时加载一次。这个例子提供了一个更好的 Singleton 实现,然而,它尽可能地延迟加载并且线程安全。此外,它适用于所有已知的 Java 版本。该解决方案是跨不同 Java 编译器和虚拟机的最可移植的解决方案。


public class Single {

private static class SingleHolder {
   private static final Single INSTANCE = new Single();
}

private Single() {
...load properties...
}

public static Single getInstance() {
    return SingleHolder.INSTANCE;
}

}

内部类的引用不早于调用 getInstance() 的那一刻(因此类加载器不早于加载)。因此,该解决方案是线程安全的,不需要特殊的语言结构(即易失性和/或同步)。

于 2008-10-22T14:33:37.653 回答
0

单个实例不是强制的(这确实不是一个好主意,因为这将避免您使用其他模式切换它的行为)。

在下面的代码中,您可以看到它是如何只实例化一次的(第一次调用构造函数时)

包装日期;

导入 java.util.Date;

公共类 USDateFactory 实现 DateFactory { 私有静态 USDateFactory usdatefactory = null;

private USDateFactory () { }

public static USDateFactory getUsdatefactory() {
    if(usdatefactory==null) {
        usdatefactory = new USDateFactory();
    }
    return usdatefactory;
}

public String getTextDate (Date date) {
    return null;
}

public NumericalDate getNumericalDate (Date date) {
    return null;
}

}

于 2009-06-13T15:14:30.513 回答