协慌网

登录 贡献 社区

在 Java 中实现单例模式的有效方法是什么?

在 Java 中实现单例模式的有效方法是什么?

答案

使用枚举:

public enum Foo {
    INSTANCE;
}

Joshua Bloch 在 Google I / O 2008 的Effective Java Reloaded演讲中解释了这种方法: 链接到视频 。另见他演示文稿的幻灯片 30-32( effective_java_reloaded.pdf ):

实现可序列化单例的正确方法

public enum Elvis {
    INSTANCE;
    private final String[] favoriteSongs =
        { "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

编辑: “Effective Java”在线部分说:

“这种方法在功能上等同于公共领域方法,除了它更简洁,免费提供序列化机制,并提供防止多个实例化的铁定保证,即使面对复杂的序列化或反射攻击。虽然这种方法有尚未被广泛采用, 单元素枚举类型是实现单例的最佳方式 。“

根据用途,有几个 “正确” 的答案。

从 java5 开始,最好的方法是使用枚举:

public enum Foo {
   INSTANCE;
}

前 java5,最简单的情况是:

public final class Foo {

    private static final Foo INSTANCE = new Foo();

    private Foo() {
        if (INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return INSTANCE;
    }

    public Object clone() throws CloneNotSupportedException{
        throw new CloneNotSupportedException("Cannot clone instance of this class");
    }
}

我们来看看代码吧。首先,你希望课程是最终的。在这种情况下,我使用了final关键字让用户知道它是最终的。然后,您需要将构造函数设置为私有,以防止用户创建自己的 Foo。从构造函数中抛出异常会阻止用户使用反射来创建第二个 Foo。然后创建一个private static final Foo字段来保存唯一的实例,并创建一个public static Foo getInstance()方法来返回它。 Java 规范确保仅在首次使用类时调用构造函数。

当你有一个非常大的对象或繁重的构造代码并且还有其他可访问的静态方法或字段可能在需要实例之前使用时,那么你只需要使用延迟初始化。

您可以使用private static class来加载实例。然后代码看起来像:

public final class Foo {

    private static class FooLoader {
        private static final Foo INSTANCE = new Foo();
    }

    private Foo() {
        if (FooLoader.INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return FooLoader.INSTANCE;
    }
}

由于行private static final Foo INSTANCE = new Foo();只有在实际使用类 FooLoader 时才会执行,这会处理惰性实例化,并保证它是线程安全的。

当您还希望能够序列化对象时,需要确保反序列化不会创建副本。

public final class Foo implements Serializable {

    private static final long serialVersionUID = 1L;

    private static class FooLoader {
        private static final Foo INSTANCE = new Foo();
    }

    private Foo() {
        if (FooLoader.INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return FooLoader.INSTANCE;
    }

    @SuppressWarnings("unused")
    private Foo readResolve() {
        return FooLoader.INSTANCE;
    }
}

方法readResolve()将确保将返回唯一的实例,即使在上一次运行程序中序列化对象时也是如此。

免责声明:我刚刚总结了所有令人敬畏的答案,并用我的话写下来。


在实施 Singleton 时,我们有 2 个选项
1. 懒加载
2. 早装

延迟加载会增加一些开销(很多是诚实的),所以只有当你有一个非常大的对象或繁重的构造代码并且还有其他可访问的静态方法或字段可能在需要实例之前使用时才使用它,然后才会你需要使用延迟初始化。否则选择早期加载是一个不错的选择。

实现 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;
    }
}

这被称为 “双重锁定成语”。很容易忘记易变的陈述,很难理解为什么有必要。
有关详细信息: http//www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

现在我们确定邪恶的线索,但残酷的序列化怎么样?我们必须确保即使在 de-serialiaztion 中也没有创建新对象

class Foo implements Serializable {

    private static final long serialVersionUID = 1L;

    private static volatile Foo INSTANCE = null;

    // 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 单例也被懒惰地初始化

这种方法在功能上等同于公共字段方法,除了它更简洁,免费提供序列化机制,并提供防止多实例化的铁定保证,即使面对复杂的序列化或反射攻击。虽然这种方法尚未被广泛采用,但单元素枚举类型是实现单例的最佳方法。

-Joshua Bloch in“Effective Java”

现在您可能已经意识到为什么 ENUMS 被认为是实施 Singleton 的最佳方式,感谢您的耐心:)
在我的博客上更新了它。