创建型模式:单例模式+5种写法+最佳实践。

什么是单例模式

单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

单例模式特点:

  1. 这个类只能有一个实例;
  2. 它必须自行创建这个实例;
  3. 它必须自行向整个系统提供这个实例。

Tips:单例模式是一种对象创建型模式。

单例模式分类

  1. 饿汉方式
  2. 懒汉方式
  3. 双重检测法
  4. 静态内部类
  5. 枚举方法

1、饿汉模式

介绍

饿汉式单例模式在这个类被加载时,就会创建一个唯一的实例。通过调用getInstance()方法来获得这个单例。

代码

Tips:由于构造方法是私有的,所以类不能被继承

public class EagerSingleton {
    // 类加载的时候创建一个实例
    private static final EagerSingleton instance = new EagerSingleton();
    // 默认构造方法为public
    // 为防止其他类直接创建对象,要将构造方法声明为私有
    private EagerSingleton(){}
    // 提供方法来返回本类的唯一对象
    public static EagerSingleton getInstance() {
        return instance;
    }
}

2、懒汉模式

介绍

懒汉模式并不是在类加载时创建实例对象,而是在调用getInstance()方法时才会创建对象。

代码

线程不安全版

// 线程不安全
public class LazySingleton {
    // 唯一实例,但不是类加载时初始化
    private static LazySingleton instance = null;

    // 覆盖原有的默认构造方法,声明为私有
    // 防止其他类直接使用构造方法创建对象
    // 同时也使子类无法继承
    public LazySingleton() {
    }

    public static LazySingleton getInstance() {
        // 如果实例为空就创建实例,正常情况下创建的语句只会执行一次
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

从代码我们可以看出,懒汉式单例模式的创建对象是在第一次企图获得单例类的对象时。

构造方法同样是私有的,这也决定了子类不能继承自这个类。

注意:懒汉式单例模式与饿汉式单例模式相比,更节省资源,但是会出现线程安全问题。

小结

  • 优点:懒加载,避免浪费资源
  • 缺点:存在线程安全问题

出现线程安全问题情况

懒汉模式并不是类加载时创建实例,而是调用getInstance()方法时才会创建对象。如果在实例还没有创建的时候,有多个线程同时调用这个方法,就可能出现线程安全问题导致创建了不止一个实例。

线程安全版1:synchronized

// 线程安全,但是效率低
public class LazySingleton2 {
    private static LazySingleton2 instance = null;
    public LazySingleton2() {}

    // 加上synchronized保证线程安全,但是效率低
    public static synchronized LazySingleton2 getInstance() {
        // 如果实例为空就创建实例,创建的语句只会执行一次
        if (instance == null) {
            instance = new LazySingleton2();
        }
        return instance;
    }
}

小结

  • 优点:懒加载,线程安全
  • 缺点:效率低

这种把synchronized声明在了方法名称前,导致之后该方法的调用都会进入同步快,这样很影响速度。下面引入双重检测机制

线程安全版2:双重检测机制

/**
 * 双重检测机制
 * 双重检测也会出现问题,具体请看
 * http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
 * https://juejin.im/post/5b9fb4e5f265da0ac07c4630
 */
public class LazySingleton3 {
    private static LazySingleton3 instance = null;
    public LazySingleton3() {}

    // 双重检测机制
    public static LazySingleton3 getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (LazySingleton3.class) {
                if (instance == null) { // 第一次检查
                    instance = new LazySingleton3();
                }
            }
        }
        return instance;
    }
}

小结

  • 优点:懒加载,线程安全,效率高
  • 缺点:指令重排问题

这段代码看起来很完美,但仍旧存在问题,以下内容引用自黑桃夹克大神的如何正确地写出单例模式

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

我们只需要将 instance 变量声明成 volatile 就可以了。

线程安全版3:双重检测改进+volatile

public class LazySingleton4 {
    // 注意这里有volatile
    private static volatile LazySingleton4 instance = null;
    private LazySingleton4(){}
    public static LazySingleton4 getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (LazySingleton4.class) {
                if (instance == null) { // 第二次检查
                    instance = new LazySingleton4();
                }
            }
        }
        return instance;
    }
}

但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

小结

  • 优点:懒加载、线程安全、效率高
  • 缺点:写法复杂

上面两个其实都比较麻烦,下面这两个才是推荐使用的方法

静态内部类(static nested class)

直接来代码

public class LazySingleton5 {
    // 静态内部类
    private static class LazyHolder {
        private static LazySingleton5 instance = new LazySingleton5();
    }
    private LazySingleton5(){}
    public static LazySingleton5 getInstance() {
        return LazyHolder.instance;
    }
}

其实这种方式本质上也是属于懒汉模式的一种。只是因为利用了静态内部类的特性,在写法上更高效,所以单独拿出来讲。

由于外部类无法访问静态内部类,因此只有当外部类调用Singleton.getInstance()方法的时候,才能得到instance实例。

注意:因为静态内部类的特性,实例并不是在该类被加载的时候创建的,而是当getInstance()方法被调用的时候。所以解决了资源浪费的问题。

根据JVM本身机制,静态内部类的加载已经实现了线程安全

优点总结

  1. 写法简洁
  2. 线程安全

你以为这样就结束了?naive

以上都模式存在的问题

反射(序列化)问题

Tips:反射出现问题,也就会导致序列化出现问题。因为序列化就是通过反射实现的。

还记得前面我们为了防止通过构造器创建对象,把构造器设置为private的吗?

就是这段代码

private Singleton(){}

但是在反射(序列化)的过程中,这段代码就失效了,因为可以强制把构造器设置为可访问

代码

public static void main(String[] args) throws Exception {
    // 获得构造器
    Constructor<LazySingleton4> constructor = LazySingleton.class.getDeclaredConstructor();
    // 把构造器设置为可访问
    constructor.setAccessible(true);
    // 创建两个实例对象
    LazySingleton instance1 = constructor.newInstance();
    LazySingleton instance2 = constructor.newInstance();
    // 比较下两个实例是否相等
    System.out.println(instance1 == instance2);
}

运行结果

序列化破坏单例

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。

但也有解决办法,就是重写readResolve()

public class Singleton implements java.io.Serializable {    
   public static Singleton INSTANCE = new Singleton();     
   protected Singleton() {}    
   private Object readResolve() {    
        return INSTANCE;    
    }   
}

是不是觉得好麻烦,那就祭出单例模式最简洁,也是最优的方法。枚举方法

哪种写单例的方式最好

在StakcOverflow中,有一个关于What is an efficient way to implement a singleton pattern in Java?的讨论:

如上图,得票率最高的回答是:使用枚举。

回答者引用了Joshua Bloch大神在《Effective Java》中明确表达过的观点:

使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

如果你真的深入理解了单例的用法以及一些可能存在的坑的话,那么你也许也能得到相同的结论,那就是:使用枚举实现单例是一种很好的方法。

最佳实践:采用枚举的方式

枚举方式优点

  1. 写法简洁
  2. 线程安全
  3. 没有反射(序列化)问题

1、写法简单

直接上代码

public enum EnumSingleton {
    INSTANCE;
}

在《Effective Java》最后推荐了这样一个写法,简直有点颠覆,不仅超级简单,而且保证了线程安全。这里引用一下,此方法无偿提供了序列化机制,绝对防止多次实例化,及时面对复杂的序列化或者反射攻击。单元素枚举类型已经成为实现Singleton的最佳方法。

当然上面只是最简洁的写法,通常我们还是采用下面这种写发

public enum EnumSingleton{
    INSTANCE;
   
    // 其实这个方法没必要,因为可以直接EnumSingleton.INSTANCE这样使用
    // 为了防止有人不会用,故加上这个方法
    public EnumSingleton getInstance() {
        return INSTANCE;
    }
    public void doSomething() {
        System.out.println("doSomething");
    }
}

测试一下

public static void main(String[] args) {
    EnumSingleton instance1 = EnumSingleton.getInstance();
    EnumSingleton instance2 = EnumSingleton.getInstance();
    System.out.println(instance1 == instance2);
    instance1.doSomething();
    instance2.doSomething();
}

运行结果

2、如何保证线程安全?

上面提到过。使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?

其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。

  1. 构造函数私有
  2. 实例唯一

2.1构造函数私有

枚举类实现其实省略了private类型的构造函数

对于第一点实际上enum内部是如下代码:

public enum Singleton {
    INSTANCE;
    // 这里隐藏了一个空的私有构造方法
    private Singleton () {}
}

2.2如何保证实例唯一

定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。

通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。

而且,枚举中的各个枚举项同时通过static来定义的。如:

public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}

反编译后代码为:

public final class T extends Enum
{
    //省略部分内容
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

了解JVM的类加载机制的朋友应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源会被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。

也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。

所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

3、如何解决反射(序列化)问题

我们再来看一下Enum这个类的声明:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable

可以看到,枚举也提供了序列化机制。某些情况,比如我们要通过网络传输一个数据库连接的句柄,会提供很多帮助。

那么,对于序列化这件事情,为什么枚举又有先天的优势了呢?答案可以在Java Object Serialization Specification中找到答案。其中专门对枚举的序列化做了如下规定:

大概意思就是:在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。

但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。

一句话总结

在序列化方面,Java中有明确规定,枚举的序列化和反序列化是有特殊定制的。这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。

3.2采用反射做个实验

public static void main(String[] args) throws Exception {
    // 获得构造器
    Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
    // 把构造器设置为可访问
    constructor.setAccessible(true);
    // 创建两个实例对象
    EnumSingleton instance1 = constructor.newInstance();
    EnumSingleton instance2 = constructor.newInstance();
    // 比较下两个实例是否相等
    System.out.println(instance1 == instance2);
}

运行结果

出现异常,说明这种方式可以阻止通过反射方式创建新对象

最后借用 《Effective Java》一书中的话,

枚举类型已经成为实现Singleton的最佳方法。

线程安全

参考:

https://juejin.im/post/5b9fb4e5f265da0ac07c4630

https://www.hollischuang.com/archives/2498

https://www.jianshu.com/p/4e8ca4e2af6c

https://stackoverflow.com/questions/70689/what-is-an-efficient-way-to-implement-a-singleton-pattern-in-java

https://juejin.im/entry/57761c1a8ac2470053246251

http://smarxpan.github.io/2016/06/29/how_to_use_singleton/

http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值