在Java中实现单例设计模式的有效方法是什么?
当前回答
免责声明:我刚刚总结了所有很棒的答案,并用自己的话写了出来。
在实施Singleton时,我们有两种选择:
延迟加载早期加载
延迟加载增加了位开销(老实说,很多),所以只有当您有非常大的对象或大量的构造代码,并且有其他可访问的静态方法或字段(可能在需要实例之前使用)时,才需要使用延迟初始化。否则,选择提前加载是一个不错的选择。
实现单例的最简单方法是:
public class Foo {
// It will be our sole hero
private static final Foo INSTANCE = new Foo();
private Foo() {
if (INSTANCE != null) {
// SHOUT
throw new IllegalStateException("Already instantiated");
}
}
public static Foo getInstance() {
return INSTANCE;
}
}
一切都很好,除了它是一个早期加载的单例。让我们尝试延迟加载的单例
class Foo {
// Our now_null_but_going_to_be sole hero
private static Foo INSTANCE = null;
private Foo() {
if (INSTANCE != null) {
// SHOUT
throw new IllegalStateException("Already instantiated");
}
}
public static Foo getInstance() {
// Creating only when required.
if (INSTANCE == null) {
INSTANCE = new Foo();
}
return INSTANCE;
}
}
到目前为止还不错,但我们的英雄在与多个邪恶线程单独战斗时将无法生存,这些线程需要我们英雄的许多实例。因此,让我们保护它免受邪恶的多线程攻击:
class Foo {
private static Foo INSTANCE = null;
// TODO Add private shouting constructor
public static Foo getInstance() {
// No more tension of threads
synchronized (Foo.class) {
if (INSTANCE == null) {
INSTANCE = new Foo();
}
}
return INSTANCE;
}
}
但这还不足以保护英雄,真的!!!这是我们能/应该做的最好的事情来帮助我们的英雄:
class Foo {
// Pay attention to volatile
private static volatile Foo INSTANCE = null;
// TODO Add private shouting constructor
public static Foo getInstance() {
if (INSTANCE == null) { // Check 1
synchronized (Foo.class) {
if (INSTANCE == null) { // Check 2
INSTANCE = new Foo();
}
}
}
return INSTANCE;
}
}
这被称为“双重检查锁定习惯用法”。人们很容易忘记不稳定的说法,也很难理解为什么有必要这样做。详细信息:“双重检查锁定失败”声明
现在我们确定了邪恶的线索,但残酷的连载呢?我们必须确保即使在反序列化时也不会创建新对象:
class Foo implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile Foo INSTANCE = null;
// The rest of the things are same as above
// No more fear of serialization
@SuppressWarnings("unused")
private Object readResolve() {
return INSTANCE;
}
}
方法readResolve()将确保返回唯一的实例,即使对象在我们的程序的上一次运行中被序列化。
最后,我们添加了足够的线程和序列化保护,但我们的代码看起来又大又难看。让我们给我们的英雄一个新形象:
public final class Foo implements Serializable {
private static final long serialVersionUID = 1L;
// Wrapped in a inner static class so that loaded only when required
private static class FooLoader {
// And no more fear of threads
private static final Foo INSTANCE = new Foo();
}
// TODO add private shouting construcor
public static Foo getInstance() {
return FooLoader.INSTANCE;
}
// Damn you serialization
@SuppressWarnings("unused")
private Foo readResolve() {
return FooLoader.INSTANCE;
}
}
是的,这是我们的英雄:)
由于行private static final Foo INSTANCE=new Foo();仅在实际使用FooLoader类时执行,这会处理延迟实例化,并保证线程安全。
我们已经走到了今天。以下是实现我们所做一切的最佳方式:
public enum Foo {
INSTANCE;
}
内部将被视为
public class Foo {
// It will be our sole hero
private static final Foo INSTANCE = new Foo();
}
就是这样!不再担心序列化、线程和丑陋的代码。ENUMS单例也被延迟初始化。
该方法在功能上等同于公共领域方法,除了它更简洁,提供了序列化机制免费提供,并提供多重保险实例化,即使面对复杂的序列化或反射攻击。虽然这种方法尚未被广泛采用,单个元素枚举类型是实现单例的最佳方式。
-《有效的Java》中的约书亚·布洛赫
现在您可能已经意识到为什么ENUMS被认为是实现单例的最佳方式,并感谢您的耐心:)
在我的博客上更新了它。
其他回答
我使用Spring框架来管理我的单件。
它并没有强制实现类的“单例性”(如果涉及多个类加载器,则无论如何都无法做到这一点),但它提供了一种非常简单的方法来构建和配置不同的工厂以创建不同类型的对象。
以下是三种不同的方法
枚举/***使用Java Enum的Singleton模式示例*/公共枚举EasySingleton{实例;}双重检查锁定/延迟加载/***带有双重检查锁定的单线模式示例*/公共类DoubleCheckedLockingSingleton{私有静态易失性DoubleCheckedLockingSingleton INSTANCE;private DoubleCheckedLockingSingleton(){}公共静态DoubleCheckedLockingSingleton getInstance(){如果(INSTANCE==null){synchronized(DoubleCheckedLockingSingleton.class){//双重检查Singleton实例如果(INSTANCE==null){INSTANCE=新的DoubleCheckedLockingSingleton();}}}返回INSTANCE;}}静态工厂方法/***带有静态工厂方法的Singleton模式示例*/公开课Singleton{//在类加载期间初始化private static final Singleton INSTANCE=new Singleton();//防止创建另一个“Singleton”实例私有Singleton(){}公共静态Singleton getSingleton(){返回INSTANCE;}}
实现单例有很多细微差别。夹持器图案不能在许多情况下使用。在使用volatile时,也应该使用局部变量。让我们从头开始,反复讨论这个问题。你会明白我的意思的。
第一次尝试可能看起来像这样:
public class MySingleton {
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
...
}
这里我们有一个MySingleton类,它有一个名为INSTANCE的私有静态成员和名为getInstance()的公共静态方法。第一次调用getInstance()时,INSTANCE成员为空。然后,流将进入创建条件,并创建MySingleton类的新实例。对getInstance()的后续调用将发现INSTANCE变量已设置,因此不会创建另一个MySingleton实例。这确保只有一个MySingleton实例在getInstance()的所有调用方之间共享。
但这种实现有一个问题。多线程应用程序在创建单个实例时具有竞争条件。如果多个执行线程同时(或前后)命中getInstance()方法,它们将分别将INSTANCE成员视为null。这将导致每个线程创建一个新的MySingleton实例,然后设置instance成员。
private static MySingleton INSTANCE;
public static synchronized MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
这里我们在方法签名中使用了synchronized关键字来同步getInstance()方法。这肯定会修复我们的种族状况。线程现在将一次一个地阻塞并进入方法。但这也造成了性能问题。这个实现不仅同步了单个实例的创建;它同步对getInstance()的所有调用,包括读取。读取不需要同步,因为它们只需返回INSTANCE的值。由于读取将构成我们调用的大部分(记住,实例化只发生在第一次调用上),因此通过同步整个方法,我们将导致不必要的性能损失。
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronize(MySingleton.class) {
INSTANCE = new MySingleton();
}
}
return INSTANCE;
}
在这里,我们将同步从方法签名移到了一个同步块,该块包装MySingleton实例的创建。但这能解决我们的问题吗?嗯,我们不再阻止读取,但我们也后退了一步。多个线程将同时或几乎同时命中getInstance()方法,它们都会将INSTANCE成员视为空。
然后,它们将到达同步块,在那里将获得锁并创建实例。当该线程退出块时,其他线程将争夺锁,每个线程将逐一通过块并创建我们类的新实例。所以我们回到了我们开始的地方。
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized(MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = createInstance();
}
}
}
return INSTANCE;
}
在这里,我们从区块内部发出另一张支票。如果INSTANCE成员已经设置,我们将跳过初始化。这称为双重检查锁定。
这解决了我们的多重实例化问题。但我们的解决方案再次提出了另一个挑战。其他线程可能无法“看到”INSTANCE成员已更新。这是因为Java如何优化内存操作。
线程将变量的原始值从主内存复制到CPU的缓存中。然后,对值的更改将写入该缓存并从中读取。这是Java的一个旨在优化性能的特性。但这给我们的单例实现带来了一个问题。第二个线程 — 由不同的CPU或内核使用不同的缓存进行处理 — 不会看到第一个所做的更改。这将导致第二个线程将INSTANCE成员视为空,从而强制创建单例的新实例。
private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized(MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = createInstance();
}
}
}
return INSTANCE;
}
我们通过在INSTANCE成员的声明上使用volatile关键字来解决这个问题。这将告诉编译器始终读取和写入主内存,而不是CPU缓存。
但这种简单的改变是有代价的。因为我们绕过了CPU缓存,所以每次对易失性INSTANCE成员进行操作时,我们都会受到性能影响 — 我们做了四次。我们再次检查存在性(1和2),设置值(3),然后返回值(4)。有人可能会说,这条路径是边缘情况,因为我们只在方法的第一次调用期间创建实例。也许创作上的表现是可以容忍的。但即使是我们的主要用例reads,也会对volatile成员进行两次操作。一次检查是否存在,再次返回其值。
private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {
MySingleton result = INSTANCE;
if (result == null) {
synchronized(MySingleton.class) {
result = INSTANCE;
if (result == null) {
INSTANCE = result = createInstance();
}
}
}
return result;
}
由于性能受到影响是由于直接对volatile成员进行操作,所以让我们将一个局部变量设置为volatile的值,并改为对局部变量进行操作。这将减少我们在易失性上操作的次数,从而收回一些损失的性能。注意,当我们进入同步块时,必须再次设置本地变量。这确保了它是最新的,以及在我们等待锁时发生的任何更改。
我最近写了一篇关于这个的文章。解构单身汉。您可以在那里找到有关这些示例和“持有者”模式示例的更多信息。还有一个真实的例子展示了双重检查的易失性方法。
我可以说是枚举单例。
在Java中使用枚举通常是声明枚举单例的一种方法。枚举单例可以包含实例变量和实例方法。为了简单起见,还要注意,如果您正在使用任何实例方法,那么如果该方法影响对象的状态,则需要确保该方法的线程安全。
枚举的使用非常容易实现,并且在可序列化对象方面没有缺点,必须通过其他方式规避这些缺点。
/**
* Singleton pattern example using a Java Enum
*/
public enum Singleton {
INSTANCE;
public void execute (String arg) {
// Perform operation here
}
}
您可以通过Singleton.INSTANCE访问它,它比在Singleton上调用getInstance()方法简单得多。
1.12枚举常量的序列化枚举常量的序列化方式不同于普通的可序列化或可外部化对象。枚举常量的序列化形式仅由其名称组成;表单中不存在常量的字段值。若要序列化枚举常量,ObjectOutputStream将写入枚举常量的name方法返回的值。要反序列化枚举常量,ObjectInputStream从流中读取常量名称;然后通过调用java.lang.Enum.valueOf方法获得反序列化的常量,并将常量的枚举类型与接收到的常量名称作为参数一起传递。与其他可序列化或可外部化对象一样,枚举常量可以用作后续在序列化流中出现的反向引用的目标。无法自定义枚举常量的序列化过程:在序列化和反序列化期间,将忽略枚举类型定义的任何类特定的writeObject、readObject、readObjectNoData、writeReplace和readResolve方法。类似地,任何serialPersistentFields或serialVersionUID字段声明也会被忽略——所有枚举类型都具有固定的serialVersionID 0L。记录枚举类型的可序列化字段和数据是不必要的,因为发送的数据类型没有变化。引自Oracle文档
传统singleton的另一个问题是,一旦实现了Serializable接口,它们就不再是单一的,因为readObject()方法总是返回一个新的实例,就像Java中的构造函数一样。这可以通过使用readResolve()并丢弃新创建的实例来避免,方法是将其替换为单例,如下所示:
// readResolve to prevent another instance of Singleton
private Object readResolve(){
return INSTANCE;
}
如果您的单例类保持状态,这可能会变得更加复杂,因为您需要使它们成为瞬态的,但是在枚举单例中,JVM保证了序列化。
好的阅读
Singleton模式枚举、Singleton和反序列化双重检查锁定和Singleton模式
确保你真的需要它。在谷歌上搜索“singleton反模式”,看看反对它的一些论点。
我想这并没有什么本质上的问题,但它只是一种公开某些全局资源/数据的机制,因此请确保这是最好的方法。特别是,我发现依赖注入(DI)更有用,特别是如果您也在使用单元测试,因为DI允许您将模拟资源用于测试目的。
推荐文章
- 如何格式化Joda-Time DateTime仅为mm/dd/yyyy?
- 如何在POM.xml中引用环境变量?
- 如何在android中复制一个文件?
- 将整数转换为字符串,以逗号表示千
- 接口方法的最终参数-有什么意义?
- Java中的@UniqueConstraint注释
- 如何在清洁模式下运行eclipse ?如果我们这样做会发生什么?
- 获取java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory异常
- Java中的正则表达式命名组
- c#和Java的主要区别是什么?
- 什么是NullPointerException,我如何修复它?
- 在Java中使用“final”修饰符
- 无法在Flutter上找到捆绑的Java版本
- 如何在Kotlin解析JSON ?
- 如何在新的材质主题中改变背面箭头的颜色?