Java中的单例模式

5

我刚刚在某个地方看到了下面的代码:

public class SingletonObjectDemo {

    private static SingletonObjectDemo singletonObject;
    // Note that the constructor is private
    private SingletonObjectDemo() {
        // Optional Code
    }
    public static SingletonObjectDemo getSingletonObject() {
        if (singletonObject == null) {
            singletonObject = new SingletonObjectDemo();
       }
       return singletonObject;
    }
}

我需要知道这部分的意义是什么:

if (singletonObject == null) {
    singletonObject = new SingletonObjectDemo();
}

如果我们不使用这部分代码会怎样?仍然会有一个SingletonObjectDemo的单例对象,那么为什么我们还需要这段代码呢?

1
只是说一下...显然,Singleton模式说构造函数应该是“protected”。这与继承有关。虽然似乎在单例中使用继承几乎不可能做得很好,但这就是模式专家们想出来的。只是说一下。 - cHao
这不适用于创建单例。如果两个或更多的线程调用getSingletonObject()方法,则可能会创建更多实例,因此可以在该方法中添加同步关键字。要使其成为单例,还应该覆盖Object clone()方法。 - Enrique
@cHao:我参加了Java专家的Kabutz博士(http://www.javaspecialists.eu/)的设计模式课程(强烈推荐!)。他说,有时候应用程序需要在各种相关的单例之间进行选择,因此有了子类化的能力。应用程序通常会在运行时选择其中一个实现类,可能是通过配置文件和`class.forName`实例化完成的。 - Carl Smotricz
@Enrique:你能解释一下这里需要clone()的原因吗?谢谢。 - Ishi
@Ishi: 最好的做法是抛出CloneNotSupportedException或类似异常(因为单例模式不应该被克隆 -- 这会导致存在两个实例 -- 但Object类具有默认的clone()方法,可以通过扩展该类并实现Cloneable接口轻松地启用该方法)。 - cHao
显示剩余3条评论
7个回答

9

关于惰性初始化和急切初始化

if语句是惰性初始化技术的一种实现方式。

更明确的版本如下:

private boolean firstTime = true;
private Stuff stuff;

public Stuff gimmeStuff() {
   if (firstTime) {
      firstTime = false;
      stuff = new Stuff();
   }
   return stuff;
}

第一次调用 gimmeStuff() 时,firstTimetrue,因此 stuff 会被初始化为 new Stuff()。在后续调用中,firstTimefalse,因此不再调用 new Stuff()

因此,stuff 被“懒惰地”初始化。直到第一次需要它时才真正初始化。

另请参阅


关于线程安全

需要说明的是,这段代码不是线程安全的。如果有多个线程,那么在某些竞态条件下,new SingletonObjectDemo() 可能会被调用多次。

一种解决方法是使 synchronized getSingletonObject() 方法。然而,这样做会对 所有getSingletonObject() 的调用产生同步开销。所谓的双重检查锁定机制被用来尝试解决这个问题,但在 Java 中,这种机制直到 J2SE 5.0 引入新的内存模型中的 volatile 关键字才真正起作用。

毋庸置疑,正确实施单例模式并不是一件简单的事情。

另请参阅

相关问题


《Effective Java》第二版

以下是书中对这些主题的讲解:

条款71:谨慎使用延迟初始化

像大多数优化一样,最好的建议是“除非必要,否则不要使用延迟初始化”。延迟初始化是一把双刃剑。它降低了初始化类或创建实例的成本,但增加了访问惰性初始化字段的成本。根据最终需要初始化的惰性初始化字段的比例、初始化它们的成本以及每个字段被访问的频率如何而定,延迟初始化(就像许多“优化”一样)实际上会损害性能。

在存在多个线程的情况下,延迟初始化是棘手的。如果两个或更多线程共享一个惰性初始化字段,则必须采用某种形式的同步,否则可能导致严重的错误。

在大多数情况下,正常初始化比延迟初始化更可取。

条款3:使用私有构造函数或枚举类型强化单例属性

从1.5版本开始,有第三种实现单例模式的方法。只需创建一个具有一个元素的枚举类型。[...] 这种方法在功能上等同于公共字段方法,但它更简洁,提供免费的序列化机制,并提供针对多次实例化的铁壁保证,即使面对复杂的序列化或反射攻击。

单元素枚举类型是实现单例模式的最佳方法。

相关问题

关于enum单例/Java实现:

关于单例模式的优点和替代方案:


5

这个类有一个字段SingletonObjectDemo singletonObject,它保存单例实例。现在有两种可能的策略 -

1 - 你可以在声明时对对象进行急切初始化 -

private static SingletonObjectDemo singletonObject = new SingletonObjectDemo();

这将导致您的单例对象在类加载时立即初始化。这种策略的缺点是,如果您有许多单例对象,它们将在尚未需要它们的情况下全部被初始化并占用内存。

2 - 您可以进行惰性初始化,即在第一次调用 getSingletonObject() 时进行初始化 -

// note that this initializes the object to null by default
private static SingletonObjectDemo singletonObject;

...

if (singletonObject == null) {
        singletonObject = new SingletonObjectDemo();
    }

这样做可以节省内存,直到真正需要单例时才会创建。这种策略的缺点是,第一次调用方法可能会看到稍微更差的响应时间,因为它必须在返回之前初始化对象。


谢谢!我正是在寻找这个答案。 - Ishi
另外,正如其他人指出的那样,如果在多线程环境中运行代码,则应使用第二种策略将getSingletonObject()方法设置为synchronized以使其线程安全。如果它没有同步,并且两个线程在对象仍为空时同时调用该方法,则两者都可能创建一个新实例。 - samitgaur
为什么在需要之前会加载类? - Rotsor
一个类被加载到JVM中的时间取决于JVM的实现。规范本身并没有指定类何时被加载。这意味着几乎不可能准确预测类何时被加载。我个人以及大多数人都会假设最坏情况的性能,并倾向于选择第二个选项。这样,我们可以确保对象在需要之前不会占用内存。 - Zoe

4
如果我们不使用这部分代码会怎样?SingletonObjectDemo 仍然只有一个实例,那么我们为什么需要这段代码呢?
其思想是懒加载单例,即仅在实际需要时加载实例。为什么要这样做?Bob Lee 在 Lazy Loading Singletons 中很好地总结了这一点:
在生产环境中,通常希望急切地加载所有单例,以便及早捕获错误并承担任何性能损失,但在测试和开发期间,您只想加载绝对需要的内容,以免浪费时间。
但是你展示的实现是有问题的,它不是线程安全的,两个并发线程可能会创建两个实例。使你的懒加载单例线程安全的最佳方法是使用Initialization on Demand Holder (IODH) idiom,这非常简单且没有任何同步开销。引用《Effective Java》中的条款71:谨慎使用延迟初始化(强调不是我的)。

If you need to use lazy initialization for performance on a static field, use the lazy initialization holder class idiom. This idiom (also known as the initialize-on-demand holder class idiom) exploits the guarantee that a class will not be initialized until it is used [JLS, 12.4.1]. Here’s how it looks:

// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}
static FieldType getField() { return FieldHolder.field; }

When the getField method is invoked for the first time, it reads FieldHolder.field for the first time, causing the FieldHolder class to get initialized. The beauty of this idiom is that the getField method is not synchronized and performs only a field access, so lazy initialization adds practically nothing to the cost of access. A modern VM will synchronize field access only to initialize the class. Once the class is initialized, the VM will patch the code so that subsequent access to the field does not involve any testing or synchronization.

参见


3

这两行代码检查是否已经创建了唯一的单例,如果没有,则会创建单例实例。如果实例已经存在,则不进行任何操作并返回该实例。单例实例是在第一次需要时按需创建的,而不是在应用程序初始化时创建。

请注意,您的代码包含竞争条件错误。当2个线程同时进入时,单例对象可能会被分配两次。可以通过像这样同步方法来解决这个问题:

public static synchronized SingletonObjectDemo getSingletonObject() {
    if (singletonObject == null) {
        singletonObject = new SingletonObjectDemo();
    }
    return singletonObject;
}

顺便提一下,回到你的问题,这行代码是:

private static SingletonObjectDemo singletonObject;

声明一个静态引用,但它实际上不会分配实例,该引用被Java编译器设置为null


1

所给的类在被请求之前不会创建对象的第一个实例。直到第一次请求,private static字段才为null,然后在那里构建和存储对象的实例。随后的请求将返回相同的对象。

如果你移除这两行代码,你实际上从来不会创建初始实例,因此你将始终返回null


0

这段代码

  • 负责创建第一个对象
  • 防止创建其他对象

0

一开始,singletonObject被设置为null。单例模式的思想是在第一次调用getSingletonObject()时初始化该对象。如果在这部分中不调用构造函数,则变量将始终为null。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接