Effective Java读书笔记之使用私有构造器强化单例

143 阅读4分钟

什么是单例

单例是指只会初始化一次,因而最多只会有一个实例的类。单例一般用来表示本质上只有一个的组件。比如操作系统中的窗体管理器和文件系统等。

单例类具备哪些要求

在使用单例时,需要考虑以下几点:

  • 访问权限控制,应当使用私有属性或方法生成实例
  • 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
  • 反序列化问题(防止多次反序列化生成多个不同的实例)
  • 线程安全(防止不同线程生成多个不同的实例)
  • 是否使用延迟加载,只在需要的时候才生成实例

如果不考虑延迟加载的问题,枚举是实现单例的最佳选择。

下面以一个完整的例子讲解在不使用枚举的情况下,做到以上几点,(除了反射攻击)。

public class TantanitLogo implements Serializable {
    private volatile static TantanitLogo singleton;
    private volatile static boolean initialized = false;

    private TantanitLogo() {
        synchronized (TantanitLogo.class) {
            if (initialized == false) {
                initialized = true;
            } else {
                throw new RuntimeException("受到反射攻击!");
            }
        }

    }

    public static TantanitLogo getInstance() {
        if (singleton == null) {
            synchronized (TantanitLogo.class) {
                if (singleton == null) {
                    singleton = new TantanitLogo();
                }
            }
        }
        return singleton;

    }

    private Object readResolve() {
        return singleton;
    }

    public static void otherMethod() {
        System.out.println("执行TantanitLogo类的静态方法otherMethod!");
        if (TantanitLogo.singleton == null) {
            System.out.println("此时singleton为null,未被初始化!");
        } else {
            System.out.println("此时singleton不为null,已被初始化!");
        }
    }
}

访问权限控制

例子中成员变量singleton和构造器都是私有类型的,实现了访问权限控制。

解决反射攻击问题(无除枚举外的其它方式)

目前除了使用枚举似乎没有其它方法可以解决反射攻击,以下代码仍然无法避免反射攻击。

private volatile static boolean initialized = false;

private TantanitLogo() {
    synchronized (TantanitLogo.class) {
        if (initialized == false) {
            initialized = true;
        } else {
            throw new RuntimeException("受到反射攻击!");
        }
    }

}

这段代码本意是,添加成员变量initialized,用来标识是否生成过实例,在调用构造函数时,如果已经调用过一次,生成过实例,则报错。但如果使用反射的方式先将initialized改为false,再调用私有构造函数,就可以顺利绕过initialized,生成第二个实例,破坏单例性。

解决反序列化问题

TantanitLogo类实现了Serializable接口,可以被序列化和反序列化,为类添加的readResolve方法,可以解决反序列化时生成新的实例的问题。

private Object readResolve() {
     return singleton;
 }

在TantanitLogoTest类添加以下测试代码:

/**
 * 测试反序列化
 */
public static void testDeserialize() throws IOException, ClassNotFoundException {
    TantanitLogo tantanitLogo1 = TantanitLogo.getInstance();


    FileOutputStream fos = new FileOutputStream("object.out");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(tantanitLogo1);
    oos.close();
    fos.close();
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.out"));
    TantanitLogo tantanitLogo3 = (TantanitLogo) ois.readObject();
    if (tantanitLogo1 == tantanitLogo3) {
        System.out.println("tantanitLogo1与tantanitLogo3是同一个实例");
    } else {
        System.out.println("tantanitLogo1与tantanitLogo3不是同一个实例");
    }
}

当TantanitLogo类中有readResolve方法时,ObjectInputStream的readObject方法会调用readResolve方法,所以输出结果为“tantanitLogo1与tantanitLogo3是同一个实例”,当TantanitLogo类中没有readResolve方法时,则输出“tantanitLogo1与tantanitLogo3不是同一个实例”。

线程安全

public static TantanitLogo getInstance() {
    if (singleton == null) {
        synchronized (TantanitLogo.class) {
            if (singleton == null) {
                singleton = new TantanitLogo();
            }
        }
    }
    return singleton;

}

上一篇文章中讲解了使用静态工厂方法代替构造器的好处,这里就是使用getInstance方法就是代替构造器,生成实例。而使用synchronized关键字达到线程安全的目的,您可能注意到,我在synchronized代码块外加了singleton == null的条件判断。这是由于只有当singleton为null时才会进行new操作,生成新的实例,所以只在这个时候对代码加同步限制。

延迟加载

以下是TantanitLogo中的另一个静态类方法:

public static void otherMethod() {
    System.out.println("执行TantanitLogo类的静态方法otherMethod!");
    if (TantanitLogo.singleton == null) {
        System.out.println("此时singleton为null,未被初始化!");
    } else {
        System.out.println("此时singleton不为null,已被初始化!");
    }
}

由于只在TantanitLogo的静态方法getInstance中进行new操作,生成新的实例。所以调用其它静态方法不会生成新的实例。在TantanitLogoTest添加方法进行测试:

/**
 * 测试延迟加载
 */
public static void testLazyLoad() {
    TantanitLogo.otherMethod();
}

输出结果为

执行TantanitLogo类的静态方法otherMethod!
此时singleton为null,未被初始化!”

满足延迟加载的要求。

您可以以每次启动调用一个测试函数的方法,对以上几个特征分别进行测试。如果您觉得哪个特征在您的应用场景中不重要,也可以很容易地进行简化。

使用枚举类实现单例

public enum TantanitLogoEnum {
    singleton
}

使用只有一个元素的枚举类可以很方便地实现单例,并且满足除了延迟加载之外的所有要求:

  • 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
  • 反序列化问题(防止多次反序列化生成多个不同的实例)
  • 线程安全(防止不同线程生成多个不同的实例)

下篇文章,将会对Enum类的工作原理进行解析,并解释为什么枚举具有这些优势。

欢迎搜索“谈谈IT”或扫描下方二维码关注微信公众号,第一时间获取最新文章(^_^)

谈谈IT