不使用单例模式有什么问题?

4

我正在阅读电子书《Head First 设计模式》中的单例模式,我知道使用该模式适用于仅需要某个类的一个实例的情况。
但是我对这本电子书中的问题介绍有些困惑。
(是的,我想我可以在这里引用它的一部分!)

巧克力工厂
每个现代化的巧克力工厂都拥有计算机控制的巧克力锅炉。锅炉的工作是将巧克力和牛奶加热至沸腾,然后将它们传输到制作巧克力棒的下一个阶段。
这是 Choc-O-Holic 公司工业强度的巧克力锅炉的控制器类。看看代码;你会注意到他们试图非常小心地确保不会发生坏事,比如排出 500 加仑未煮沸的混合物,或者在锅炉已满时灌满锅炉,或者将空锅炉煮沸!

public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;

    private ChocolateBoiler() {
        empty = true;
        boiled = false;
    }

    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
            // fi ll the boiler with a milk/chocolate mixture
        }
    }

    public void drain() {
        if (!isEmpty() && isBoiled()) {
            // drain the boiled milk and chocolate
            empty = true;
        }
    }

    public void boil() {
        if (!isEmpty() && !isBoiled()) {
            // bring the contents to a boil
            boiled = true;
        }
    }

    public boolean isEmpty() {
        return empty;
    }

    public boolean isBoiled() {
        return boiled;
    }
}

是的,这是他们的问题:

Choc-O-Holic已经做了相当不错的工作,确保不会发生坏事,你不这么认为吗?但如果两个ChocolateBoiler实例失控,那么你可能会怀疑会发生很糟糕的事情。
如果在应用程序中创建多个ChocolateBoiler实例,可能会出现什么问题?

因此,当我们这样做时,问题就会“发生”:

ChocolateBoiler boiler1 = new ChocolateBoiler(),
 boiler2 = new ChocolateBoiler();
//...

但我发现这两个实例控制它自己的行为,并且它们独立运行(因为这里没有静态字段)。因此,它们分别运行而不会影响其他实例。我想知道这个问题是关于非法状态或者当一个实例运行并影响到其他实例时可能发生的情况(来自电子书的“错误程序行为、过度使用资源或不一致的结果”),但这并不是这里的情况。
那么,“事情怎么会出错呢?”,难道只是浪费实例吗?
引用:“如果有两个ChocolateBoiler实例失控,那么会发生一些非常糟糕的事情。”
我想看看那些“糟糕的事情”会如何发生?
#编辑1: 感谢大家帮助我。我解决了我的问题。 当我调用“boiler2 = new ChocolateBoiler()”时,“boiler2”实例仍然指向与“bolder1”相同的锅炉,对吗? 第一次我认为“new ChocolateBoiler()”类似于购买一个新锅炉:) 这是关于概念,我是一个新手。

只有一个锅炉。当两个东西尝试控制单个锅炉时会发生什么? - Dave Newton
是的,试图控制单个锅炉的两个事物可能会导致非法状态,但他们的问题是当创建多个ChocolateBoiler实例时会出现什么问题? - phibao37
1
有一个锅炉,有两个东西试图控制它。 - Dave Newton
1
这是我不喜欢单例的许多原因之一。问题不在于您需要确保只有一个实例,而在于您需要每个锅炉一个实例。如果您为每个锅炉创建一个实例并适当地绑定它们,那么它将是可以接受的。 - Brandon
1
这怎么能编译通过呢?考虑到构造函数是私有的:ChocolateBoiler boiler1 = new ChocolateBoiler() 不应该是可能的。 - John
显示剩余3条评论
3个回答

6
您似乎不理解这个例子试图解释的概念。 ChocolateBoiler 不是真正的锅炉,而是一个Java类。
然而,可以使用此类来指示硬件(真正的锅炉控制器)执行某些操作。如果您错误地拥有两个 ChocolateBoiler 实例,并且错误地使用它们来指示相同的锅炉控制器,那么显然会遇到问题。
在我之前的段落中有两个“错误”,您可能会认为如果一般情况下做事情出现“错误”,那么您无论如何都会遇到麻烦。但是,在设计不良的单例模式的情况下,错误可能不太明显。如果您序列化和反序列化未处理序列化问题以保持唯一性的单例,并尝试使用该实例加热锅炉,则可能会将锅烧毁。

4

单例模式有一些问题需要注意。下面我们来看两个不同的单例示例:

1)无状态单例

这个单例没有字段成员,只提供方法作为对外服务。

public class StatelessSingleton {

    private static final StatelessSingleton INSTANCE = new StatelessSingleton();

    private StatelessSingleton() {

        // exists to defeat instantiation
    }


    public void service() {
        //...
    }

    public void anotherService() {
        //..
    }

    public StatelessSingleton getInstance() {
        return INSTANCE;
    }
}

这种单例设计模式通常可以用只有静态方法的类替代,因为这样更加高效且易读。唯一的例外是在实现 Strategy 策略模式等需要实现无状态算法接口时,缓存该实现会很有意义。当然,要实现接口,你就需要一个实例。

2) 有状态的 Singleton

public class StatefullSingleton {

    private int a = 3;

    private static final StatefullSingleton INSTANCE = new StatefullSingleton();

    private StatefullSingleton() {

        // exists to defeat instantiation
    }


    public void service() {
        // do some write operation on a
    }

    public void anotherService() {
        // do some read operation on a
    }

    public StatefullSingleton getInstance() {
        return INSTANCE;
    }

}

现在我们来讨论单例模式的问题:

当单例模式实现不当时,可能会出现多个实例的情况。举个例子,如果你在Java中使用双重检查锁定来确保只有一个Singleton实例存在,就可能会出现这种情况:

class Foo {
    private Helper helper;
    public Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }

    // other functions and members...
}

有很多资源可以讨论为什么这在Java中不起作用,所以这里不需要重复。

避免双重检查锁定问题的一种方法是如上所示,使用私有构造函数和静态引用实例+ getter。

第二个问题,特定于StatefullSingelton,如果您的服务方法未同步,则多个线程可能会破坏此类Singleton的状态。在您的示例中,如果不同的工作人员同时填充和排放锅炉,则可能会出现问题。

第三个问题是序列化。鉴于Singleton实现了java.io.Serializable接口,这可能会导致在反序列化期间创建多个Singleton。为了避免在反序列化时创建新对象,必须实现readResolve


Java 单例模式的实现分析很不错。 - Nikos M.
关于你的第二个问题:我很惊讶地发现,在上面的ChocolateBoiler示例中,即使Singleton被正确实现,两个指向相同单例ChocolateBoiler的工作线程几乎可以同时填充。如果运气不好,在empty = false执行之前(两次)都可能调用isEmpty(),然后两个工作线程将愉快地尝试填充锅炉。 - Eric Duminil

2
使用单例模式的整个重点与“单一真相来源”原则相关,可以视为其变体。当然,“单一真相来源”也是一种冲突管理方式。
单例模式的另一个方面是出于效率考虑需要尽量减少不必要的复制和/或重新初始化。
例如,两个独立的实例会在同一资源上发生冲突,这取决于应用程序和所使用的平台(如多线程),可能会导致各种问题,如死锁、无效状态等。
资源是唯一的(单例),因此管理器或驱动程序需要考虑到这一点,以避免对同一资源的潜在冲突(见上文)。

谢谢您的回答,但您能解释一下我例子中的“问题”吗? - phibao37
两个独立的锅炉实例会在同一资源上发生冲突。 - Nikos M.
1
这可能会导致各种问题(取决于所使用的应用程序和平台),例如死锁、无效状态等。 - Nikos M.
在这个示例版本中,这些实例之间没有共享任何资源。@Nikos - phibao37
不行,它们试图控制同一个锅炉,如果一个实例检查isEmpty()然后睡眠,另一个实例接管锅炉并关闭,那么第一个实例将再次接管并重新煮沸(无效状态)。 - Nikos M.
1
这正是问题所在,它们不共享状态,因此它们自己的状态可能会发生冲突,因为它们在不同的时间引用相同的资源,所以它们可能会失去同步,正是因为它们不共享状态。 - Nikos M.

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