Effective Java学习笔记(三)单例模式

本文对应原书条目3,原书仅仅提到了如何实现单例模式,本文想在此基础上作必定的拓展,力求较为全面地介绍单例模式,探讨单例模式的应用场景、优缺点及多种实现方式,以及如何防范序列化和反射致使的安全性问题。若有问题或建议,欢迎指教,谢谢~html

什么是单例模式

单例模式是一个只会被实例化一次的类,它会自行实例化,并提供可全局访问的方法。java

单例模式的适用场景

  • 一个系统中只须要存在一个的对象,例如文件管理器
  • 须要频繁适用但建立成本过高的对象,如数据库的链接

单例模式的实现方式

有三种实现单例的方式,公共静态不可变成员、静态工厂方法和枚举。前两种比较相似,都是经过私有构造方法+公共静态成员的方式提供单例。而第三种枚举的方式是在Java1.5之后引入的,事实上咱们在后面会看到这是Java语言实现单例的最佳实践。程序员

公共静态不可变成员

这种方式的具体实现以下:数据库

public class Singleton {
    public static final Singleton INSTANCE = new Singleton();

    private Singleton() {}
}

这种方式实现起来比较简单,并且能够清楚地标明这是个单例类,可是缺点正如第一篇学习笔记中提到的,对它的访问不如静态工厂方法来得清晰,因此就有了下面使用静态工厂方法实现单例的方式。设计模式

静态工厂方法

咱们先来看看最典型的静态工厂方法实现的单例。数组

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

静态工厂方法实现单例有几个好处[1]。首先,它具有灵活性,在不改变对外发布的API的前提下,咱们能够改变它内部的实现,好比从单例变成非单例,或是每一个线程一个单例。其次,它的可扩展性强,可使用泛型单例工厂的方式提供单例的访问(这个咱们之后再讨论)。最后是它的便利性,能够支持方法引用,像Singleton::instance这样。安全

使用静态工厂方法实现单例有不少种玩法[2],上面那种被称为饿汉式,它的优势是线程安全、便于使用;缺点是应用初始化时较慢,若是这个单例对象一直没有使用,会浪费内存空间。多线程

下面是它的一个变种:并发

public class Singleton {
    private static final Singleton INSTANCE;

    static {
        // 一些前置操做
        INSTANCE = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

这种变种其实就是把单例对象的初始化过程放到了静态代码块中,优缺点同上。我理解主要适用于实例化单例类须要一些前置操做的状况。性能

除此以外,还有其余的写法——

1. 懒汉式

public class Singleton {
    private static final Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

懒汉式将初始化单例变量的时机放在了第一次调用的时候(懒加载),这样作的优势在于能够加快启动速度,且不会像饿汉式那样形成可能的内存空间浪费,可是缺点在于没法保证线程安全性。

2. 懒汉式变种

public class Singleton {
    private static final Singleton INSTANCE;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

这一变种形式的优势同上,并解决了上面的线程不安全问题,可是缺点在于对getInstance()方法进行了同步,并发性能较差。

3. 双重检查锁

public class Singleton {
    // 这里加了volatile关键字修饰
    private static volatile final Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() {
        // 双重检查
        if (INSTANCE == null) {
            synchronized(Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

这种方式的优势是在保证线程安全的前提下提升了多线程访问的性能。由于采用了volatile关键字+代码块加锁+两次是否null检查,当一个线程初始化了INSTANCE后,其余线程立刻可见了。它的缺点是实现起来比较复杂。

4.静态内部类

public class Singleton {

    private Singleton() {}

    private static class SingletonHolder() {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

这种方式本质上也是懒加载的,拥有懒加载方式的优势。它采用类加载的机制实现懒加载和保证线程安全,只有第一次调用getInstance()方法的时候才会装载内部类SingletonHolder

5.枚举

public enum Singleton {
        INSTANCE;
        public void yourOwnMethod() {}
    }

你或许会以为枚举这种方式很奇怪,可是它事实上兼具了上述全部的优势,加载效率高,并发性能好,并且易于编写。而且在后面咱们还能够看到,它的安全性也很是高,不须要咱们采起额外的防范。

单例模式的安全问题

有一些手段可以破坏类的单例模式,好比经过序列化反射的方式。

序列化破坏单例

Java语言的序列化主要依靠ObjectOutputStreamObjectInputStream这两个类。前者负责将对象序列化为二进制数组,然后者负责反序列化。经过ObjectOutputStreamwriteObject()方法将单例对象写入外部文件,再经过ObjectInputStreamreadObject()方法从外部读取一个二进制数组进来写入单例中,这个时候单例就成了另一个对象了。以下面的代码所示[2]

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton singleton1 = Singleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_bin_file"));
        oos.writeObject(singleton1); // 序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_bin_file"));
        Singleton singleton2 = (Singleton) ois.readObject(); // 反序列化
        System.out.println(singleton1 == singleton2); // 会返回false
    }
}

为了防止被这种方式攻击,咱们能够在单例类中加入readResolve()方法。以下所示:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }

    private Object readResolve() {
        return INSTANCE;
    }

}

为何这样可行呢?由于Java的序列化机制在容许类本身实现一个readResolve()方法,在ObjectInputStream执行了readObject()以后,若是存在readResolve()方法,则会调用,并对readObject()的结果进行处理,以后做为最终的结果返回。像咱们上面那样在readResolve()中返回了本来的INSTANCE,这样就能保证不会因readObject()生成新的对象,从而确保了单例机制不被破坏[2]

另外,若是单例中有成员变量,应当声明为transient类型[1],这样,在序列化的时候会跳过这个字段,而反序列化时会得到一个默认值或者null。我理解这样作的目的是保护单例的成员变量,不让它们泄露出去,也不会被乱赋值。没有值总比被赋了错值要好。

反射破坏单例

反射对单例的破坏主要是经过调用成员变量或者构造方法的setAccessible()方法,来访问本来private的变量或者方法,从而破坏了单例模式。

对反射攻击的防护能够经过在构造方法中增长校验的方式实现,以下所示:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
        if (INSTANCE != null) {
            throw new RuntimeException("INSTANCE already exists!");
        }
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

这种方式只对饿汉式单例实现有效,而对懒汉式无效。由于前者的单例在类加载时即被初始化了,类加载的时机必定是在反射前的;然后者则是在getInstance()被调用时才初始化单例,不能保证在反射以前执行。[2]

最好的单例模式实现

不管是经过公共静态不可变成员仍是静态工厂方法来实现单例,都有缺陷,须要程序员本身去保证性能和安全。然而,正如前面所看到的,还有一种更好的方式来实现单例,那就是枚举

public enum Singleton {

        INSTANCE;

        private String yourOwnField;

        public String getYourOwnField() {
            return yourOwnField;
        }

        public void setYourOwnField(String yourOwnField) {
            this.yourOwnField = yourOwnField;
        }

        public void yourOwnMethod() {}
    }

枚举有以下几个优势[2]

  • 写法简单
  • 线程安全:编译成class文件后的枚举类中,INSTANCE变量会被public static final修饰,而静态变量会在类加载时被初始化,所以JVM会保证其线程安全性。
  • 懒加载:JVM会在类被引用到的时候才去加载它,因此枚举自带懒加载效果
  • 避免序列化攻击:在序列化枚举类型时,Java仅会序列化枚举对象的name,而后在反序列化时根据这个name获得具体的枚举对象,因此是能够自然防护序列化攻击的。
  • 避免反射攻击:反射不容许建立枚举对象
序列化、反射和枚举这几部分参考资料[2]中讲得很透彻,建议你们阅读下~

总结

单例模式提供了对某一对象的受控访问,适用于不少场景。用枚举来实现单例是最好的方式。下面是单例模式的优缺点[2][3]

优势

  • 节省频繁建立和销毁对象的性能开销
  • 实现对某些临界资源的单一受控访问

缺点

  • 单例机制没法被继承
  • 违背了单一职责原则,单例类既要维护单例逻辑,又要实现其余内部逻辑
  • 当一个单例对象长期未被访问,可能会被GC,这样一些共享数据就丢失了
小小的感慨:虽然Effective Java上面这个条目的内容很是少,可是本身去深挖之后发现竟然有这么多值得研究的东西。我的感受书上讲得仍是太简单,好多地方都没有讲透,也没有相应的例子。仍是得靠本身多搜索资料,多思考,才能吃透一块知识。

声明

本文仅用于学习交流,请勿用于商业用途。转载请注明出处,谢谢。

参考资料

  1. 《Effective Java(第3版)》
  2. 设计模式 | 单例模式及典型应用 https://www.jianshu.com/p/8f6...
  3. 单例模式 https://www.runoob.com/design...
相关文章
相关标签/搜索