一、定义

确保每一个类只有一个实例,且类自己实例化这个类

二、使用场景

比如生成唯一的序列号、项目中需要一个共享的资源、spring中的bean默认单例.

三、优缺点

  • 优点

    1.内存中只有一个实例,减小内存开支,优化和共享资源访问。

  • 缺点

    1.没有接口,扩展不便(接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的)。

四、代码

1.饿汉式

在初始化时直接创建。

public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){};

    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

优点:线程安全

缺点:消耗资源,不需要实例的时候就生成实例了,我们要的是什么时候需要才生成。

2、懒汉式

在使用时创建对象

public class LazySingleton {
    private LazySingleton lazySingleton = null;

    private LazySingleton(){};

    public LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton =  new LazySingleton();
        }
        return lazySingleton;
    }
}

存在问题

  • 在多现场情况下,如果线程A判断为空,进入了if代码块,还没有new出instance,线程B也判断为null进入了if代码块,则又会new instance并返回。这时候内存中就不唯一实例了。

解决方案

在获取实例的方法上加synchronized解决线程不安全问题。

public synchronized LazySingleton2 getInstance() {
        if(lazySingleton2 == null){
            lazySingleton2 = new LazySingleton2();
        }
        return lazySingleton2;
    }

存在问题

线程问题能够解决,但是我每次去获得实例时,都要在那同步排队获得锁,效率太低。

②使用双重校验实现DCL

public LazySingleton2 getInstance3() {
        if(lazySingleton2 == null){
            synchronized (LazySingleton2.class){
                if(lazySingleton2 == null){
                    lazySingleton2 = new LazySingleton2();
                }
            }
        }
        return lazySingleton2;
    }

解析:

DCL方式将同步方法改成了同步代码块,锁的粒度更小,并发更好。

为什么要判断两次instance==null呢?

第一次检测:

由于单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,每次都要去竞争锁。

第二次检测:

如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

主要都是防止创建多个实例。

存在问题:因为有指令重排优化性能所以new一个对象大概会走三步。

  • 1.分配内存空间:memory = allocate();

  • 2.初始化对象:instance(memory)

  • 3.设置instance指向刚分配的内存地址.

由于2,3是没有依赖关系的,23顺序不改变执行结果,这种重排序优化是允许的。

指令重排后,当一条线程访问instance不为null时,由于instance未必已经初始化完成,也就造成了线程安全的问题

解决方式:

对instance以volatile修饰。

对于被volatile修饰的变量的写会禁止其重排序。

public class SingletonLazyDCLVolatile {
    private static volatile SingletonLazyDCLVolatile instance;
    private SingletonLazyDCLVolatile(){}
    public static SingletonLazyDCLVolatile getInstance(){
        if(instance == null){
            synchronized (SingletonLazyDCLVolatile.class){
                if(instance == null){
                    instance = new SingletonLazyDCLVolatile();
                }
            }
        }
        return instance;
    }
}