单例模式(Singleton Pattern),其定义如下:

Ensure a class has only one instance, and provide a global point of accessto it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

单例模式的优点

由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。例如,通过缓存的Json字符串频繁创建对象。

单例模式的缺点

  1. 扩展困难。若要扩展,除了修改代码基本上没有第二种途径可以实现。
  2. 对测试不友好。由于单例基本不存在接口(规定接口,mock对象实现接口进行测试),因此在并行开发的环境中,无法通过mock的方式虚拟一格对象。

单例使用场景

  1. 要求生成唯一序列号的环境;
  2. 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
  3. 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
  4. 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。

单例模式的注意事项

  1. 线程安全问题
  2. 对象复制的情况。在Java中,Class实现了Cloneable接口并实现了clone()方法,那么就可以复制对象,且对象复制的过程是通过内存直接复制的,不会调用类的构造函数。解决该问题的最好方法就是不要实现Cloneable接口。

单例模式的7种实现

1. 懒汉(线程不安全)

  1. public class Singleton {
  2. private static Singleton instance;
  3. private Singleton (){} //私有构造函数
  4. public static Singleton getInstance() {
  5. if (instance == null) {
  6. instance = new Singleton();
  7. }
  8. return instance;
  9. }
  10. }

2. 懒汉式(线程安全)

  1. public class Singleton {
  2. private static Singleton instance;
  3. private Singleton (){}
  4. public static synchronized Singleton getInstance() {
  5. if (instance == null) {
  6. instance = new Singleton();
  7. }
  8. return instance;
  9. }
  10. }

每次调用getInstance()都需要获取锁,效率太低,加锁的目的只是保证创建instance实例的安全性,当instance实例创建之后完全没有必要再去获取锁了。

3. 饿汉式单例

  1. public class Singleton {
  2. private static Singleton instance = new Singleton();
  3. private Singleton (){}
  4. public static Singleton getInstance() {
  5. return instance;
  6. }
  7. }

由于使用了static关键字,保证了在引用这个变量时,关于这个变量的所以写入操作都完成,所以保证了JVM层面的线程安全。

但是不能实现懒加载,造成空间浪费,如果一个类比较大,我们在初始化的时就加载了这个类,但是我们长时间没有使用这个类,这就导致了内存空间的浪费。

4. 饿汉式单例(静态代码块实现)

  1. public class Singleton {
  2. private static Singleton instance = null;
  3. static {
  4. instance = new Singleton();
  5. }
  6. private Singleton (){}
  7. public static Singleton getInstance() {
  8. return instance;
  9. }
  10. }

5. 双重校验锁

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                // 注意此处还得有次判空~
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}

这种写法在去除了方法上的同步锁,采用互斥锁去创建实例。假设同时有两个线程通过了第一次检查,进入到了互斥锁,线程A获取到这个锁,线程B则阻塞等待。线程A执行初始化对象后,释放锁。线程B获得锁,由于有二次检查,实例已初始化就不会去再次初始化对象。


此时singleton必须使用volatile修饰!!!

volatile关键字有两个作用:

  1. 保持线程可见性
  2. 禁止指令重排序

创建对象的过程有三个步骤:

  1. 半初始化,申请内存,值为默认值
  2. 设初始值
  3. 将INSTANCE对象指向分配的内存空间

如果有一线程A执行到 singleton``= new Singleton() 语句,发生了指令重排,步骤二和步骤三颠倒了:

  1. 半初始化,申请内存,值为默认值
  2. singleton对象指向分配的内存空间(此时singleton非null)
  3. 设初始值

先将singleton对象和内存空间建立联系,此时为默认值。线程B又继续进行,发现singleton非空,于是直接返回singleton,造成各种不可预知的后果。因此在DCL单例模式中,必须要在实例上volatile关键字。


6. 静态内部类

public class Singleton {  
    // 静态内部类
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  

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

静态内部类不会因为外部类的加载而加载,而是在主动使用的时候才会加载。因此,只有显示调用getInstance方法时,才会显式加载SingleHolder类,既达到了懒加载的效果,也实现了线程安全。


注意:静态内部类方式算是比较完美的实现了,既是线程安全的,又是懒加载,相对于双重校验锁实现,写法有比较简单。但是,由于Java存在反射、克隆和序列化等机制,导致这些实现都不在安全,可以创建出新的对象。

6.1 反射破坏单例模式
    Singleton single = Singleton.getInstance();
    Constructor<Singleton> dc = Singleton.class.getDeclaredConstructor();
    dc.setAccessible(true);
    Singleton singleCopy = dc.newInstance();
    //false,单例被破坏
    System.out.println(singleCopy == single);

防止通过反射破坏单例模式(在构造方法中判断instance是否为空从而抛出异常):

    private Singleton() {
        synchronized (Singleton.class) {
            if (instance != null) {
                throw new RuntimeException("Not allow to initialize again");
            }
        }
    }

运行报错:
image.png

刚才是通过getInstance()方法创建了instance实例,导致通过反射创建的时候,instance实例已经存在故而报异常。但是,如果直接通过反射创建两个对象呢?

    public static void main(String[] args) throws Exception {
        // Singleton single = Singleton.getInstance();
        Constructor<Singleton> dc = Singleton.class.getDeclaredConstructor();
        dc.setAccessible(true);
        Singleton single = dc.newInstance();
        Singleton singleCopy = dc.newInstance();
        //false,单例被破坏
        System.out.println(singleCopy == single);
    }

解决方法,在构造其中设置标志位:

    static boolean initialized = false;

    private Singleton() {
        if (!initialized) {
            initialized = true;
        } else {
            throw new RuntimeException("Not allow initialized again!");
        }
    }

直接通过反射创建单例,直接报错:
image.png

问题好像解决了,但是,如果通过反射直接修改_initialized_的值,那这个判断又将失效:

    public static void main(String[] args) throws Exception {
        // Singleton single = Singleton.getInstance();
        Constructor<Singleton> dc = Singleton.class.getDeclaredConstructor();
        dc.setAccessible(true);
        Singleton single = dc.newInstance();

        //再次通过反射修改属性值
        Field flag = Singleton.class.getDeclaredField("initialized");
        flag.setAccessible(true);
        flag.set(dc,false);

        Singleton singleCopy = dc.newInstance();
        //false,单例被破坏
        System.out.println(singleCopy == single);
    }

这样看来,标志位也行不通了。那么,还有没有更安全的办法呢?答案是有的,那就是使用枚举创建单例

序列化破坏单例模式
    public static void main(String[] args) throws Exception {
        Singleton s = Singleton.getInstance();

        byte[] serialize = SerializationUtils.serialize(s);
        Object deserialize = SerializationUtils.deserialize(serialize);


        System.out.println(s); 
        System.out.println(deserialize);
        System.out.println(s == deserialize);
    }

com.bujian.designpatterns.Singleton@327471b5 com.bujian.designpatterns.Singleton@36f6e879 false

那怎么解决这种情况呢?答案是在Singleton类中添加以下方法:

    private Object readResolve() {
        return instance;
    }

再次运行代码,结果如下:

com.bujian.designpatterns.Singleton@327471b5 com.bujian.designpatterns.Singleton@327471b5 true

序列化内部也是通过反射获取对象的。readResolve方法只是系统提供的一个钩子,用来替换反射得到的新对象的。也就是说,其实系列化还是产生了新对象的,只不过被readResolve方法提供的实例拦截了,没用上。

7. 枚举

public enum Singleton {  
    INSTANCE;  
}

这种方式是Effective Java作者Josh Bloch提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象

为啥枚举实现被称为最好的单例实现?

前几种方式实现单例都有如下3个特点:

  1. 构造方法私有化
  2. 实例化的变量引用私有化
  3. 获取实例的方法共有

这些实现方式都有一个共同的问题:私有化构造器并不保险。因为它抵御不了反射攻击。
**
其中防止方法已经在上文中给出,但是并不完美。因为不管是在构造函数在被第二次调用的时候抛出异常,还是采用标志位的方式,都可以通过多次反射破坏掉单例。要想解决这个问题,还得从newInstance()方法中寻找答案:

    public T newInstance(Object... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        if (!this.override) {
            Class<?> caller = Reflection.getCallerClass();
            this.checkAccess(caller, this.clazz, this.clazz, this.modifiers);
        }

        if ((this.clazz.getModifiers() & Modifier.ENUM) != 0) {
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        } else {
            ConstructorAccessor ca = this.constructorAccessor;
            if (ca == null) {
                ca = this.acquireConstructorAccessor();
            }

            T inst = ca.newInstance(initargs);
            return inst;
        }
    }

主要是这一句:(clazz.getModifiers() & Modifier.ENUM) != 0。说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败,因此枚举类型对反射是绝对安全的。

综上,可以得出结论:枚举是实现单例模式的最佳实践。毕竟使用它全都是优点:

  1. 反射安全
  2. 序列化/反序列化安全
  3. 写法简单

参考

序列化与单例模式

单例模式大全,反射爆破拆解!你要的8种单例都在这!

Java单例模式的7种写法中,为何用Enum枚举实现被认为是最好的方式?

SerializationUtils工具类源码