《Effective Java》笔记03:用私有构造器或者枚举类型强化Singleton属性

Singleton 即单例,指仅仅被实例化一次的类。

私有构造器强化

通过私有化构造函数,使客户端不能通过 new 关键字的形式来创建对象,同时我们需要提供静态工厂方法来使得外部能够通过调用调用静态方法取得该类的唯一实例。

延迟初始化的好处是:降低了初始化类或创建实例的开销。

延迟初始化的坏处是:增加了访问被延迟初始化的字段的开销。

如果对象的初始化的开销非常昂贵,而且多个线程不会频繁调用 getInstance() 来获取被延迟初始化的对象时,可以考虑使用延迟初始化。
一般情况下,正常的初始化要优于延迟初始化

饿汉式

class Single{ //类一加载,对象就已经存在了
    private static final Single s = new Single();
    private Single(){}
    public static Single getInstance() {
        return s;
    }
}

懒汉式

class Single{ //类加载进来,没有对象,只有调用了getInstance方法时,才会创建对象。
    //延迟加载形式。
    private Single s;
    private Single(){}
    public static Single getInstance() {
        if (s == null) {
            s = new Single();
        }
        return s;
    }
}

懒汉式解决多线程安全问题

//加入同步为了解决多线程安全问题。
//加入双重判断是为了解决效率问题。
class Single
{
    private static Single s = null;
    private Single(){}
    public static Single getInstance() {
        if(s == null) {
            synchronized(Single.class) {
                if (s == null)
                    s = new Single();
            }
        }
        return s;
    }
}

上面的代码在多线程还是有可能出现问题,详情参见双重检查锁定与延迟初始化
解决方法可以使用下面两种

基于 volatile 的双重检查锁定的解决方案

public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();//instance 为 volatile,现在没问题了
            }
        }
        return instance;
    }
}

基于类初始化的解决方案

public class A {  
    // 私有静态内部类, 只有当有引用时, 该类才会被装载  
    private A(){ }
    private static class LazyA {  
        public static A A = new A();  
    }  
    public static A getInstance() {  
        return LazyA.A;  //这里将导致LazyA类被初始化
    }  
}

延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。
如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化的方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案

防止反射机制调用私有构造器

享有特权的客户端可以借助 AccessibleObject.setAccessible 方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。

public class Single {
    private final static Single SINGLETON = new Single();
    private Single(){
        if(SINGLETON != null){
            throw new IllegalArgumentException("不能存在第二个实例");
        }
    }
    public static Single getInstance(){
        return SINGLETON;
    }

    @SuppressWarnings("rawtypes")
    public static void main(String[] args) throws Exception {
        Single s = Single.SINGLETON;
        // 利用反射调用私有构造器
        Constructor[] arrayConstructor = s.getClass().getDeclaredConstructors();
        for (Constructor constructor : arrayConstructor) {
            // 调用setAccessible(true);
            constructor.setAccessible(true);

            // 实例化,这里一定会抛出异常
            constructor.newInstance();
        }
    }
}

Singleton 序列化

为了使 Singleton 类变成可序列化的,仅仅在声明中加上 implements Serializable 是不够的。为了维护并保证 Singleton,必须声明所有实例都是瞬时 transient 的(非静态数据不想被序列化可以使用这个关键字修饰),并提供 一个 readResolve 方法。否则每次反序列化一个序列化的实例时,都会创建一个新的实例。

private Object readResolve() {
    return SINGLETON;
}

使用枚举类型来实现单例

从Java 1.5发行版本起,实现 Singleton 还有另一种方法。只需编写一个包含单个元素的枚举类型。

// https://stackoverflow.com/questions/70689/what-is-an-efficient-way-to-implement-a-singleton-pattern-in-java
// 第77(89)条:对于实例控制,枚举类型优先于 readResolve
public enum Elvis {
    INSTANCE;
    private final String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}
public enum SingleEnum {
    SINGLETON;
}

public class Test {
    public static void main(String args[]) throws Exception {
        // 测试,是否可以反射生成枚举
        // 利用反射调用私有构造器
        Constructor[] arrayConstructor = SingleEnum.SINGLETON.getClass().getDeclaredConstructors();
        for (Constructor constructor : arrayConstructor) {
            // 调用setAccessible(true);
            constructor.setAccessible(true);
            // 实例化,这里一定会抛出异常
            constructor.newInstance();
// 报错: Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects.
        }
    }
}

这种方法在功能上与公有域方法相近,但是其更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛使用,但是单元素的枚举类型已经成为实现 Singleton 的最佳方法

然而,Android 并不推荐使用枚举,相对于使用静态常量而言,它会消耗两倍以上的内存 Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Effective Java》笔记03:用私有构造器或者枚举类型强化Singleton属性

文章字数:1.3k

本文作者:Bin

发布时间:2016-05-06, 14:52:19

最后更新:2019-08-06, 00:42:29

原始链接:http://coolview.github.io/2016/05/06/Effective-Java/%E3%80%8AEffective%20Java%E3%80%8B%E7%AC%94%E8%AE%B003%EF%BC%9A%E7%94%A8%E7%A7%81%E6%9C%89%E6%9E%84%E9%80%A0%E5%99%A8%E6%88%96%E8%80%85%E6%9E%9A%E4%B8%BE%E7%B1%BB%E5%9E%8B%E5%BC%BA%E5%8C%96Singleton%E5%B1%9E%E6%80%A7/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录