天天看点

单例模式

单例及单例模式概念

一个类只允许创建一个实例。这种设计模式就是单例模式

单例模式

实现单例模式需要考虑什么?

  • 构造函数私有,避免外部通过new来创建实例
  • 考虑是否需要延时加载
  • 考虑线程安全

单例模式的几种实现方式

1. 饿汉式:在类加载的时候,就已经初始化实例了。

public class Hungry {
  // 一启动就被加载,还未使用就占用内存,造成资源浪费
  private static final Hungry INSTANCE = new Hungry();
  
  // 构造方法私有,外部无法进行实例化
  private Hungry() {}
  
  // 获取实例对象
  public static Hungry getInstance() {
    return INSTANCE;
  }
}      

2. 懒汉式:第一次使用时才进行实例化

public class Lasy {
    // 启动时不加载,需要时再进行实例化
    private static Lasy INSTANCE;
    // 构造方法私有,外部无法进行实例化
    private Lasy() {}
    public static Lasy getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Lasy();
        }
        return INSTANCE;
    }
}      

但是懒汉式并不能保证单例,在多线程并发情况下,会多次调用构造方法进行实例化,示例如下:

public class Lasy {
    // 启动时不加载,需要时再进行实例化
    private static Lasy INSTANCE;
    // 构造方法私有,外部无法进行实例化
    private Lasy() {
        System.out.println(Thread.currentThread().getName());
    }
    public static Lasy getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Lasy();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(Lasy::getInstance).start();
        }
    }
}      

输出结果:

单例模式

2.1 使用同步锁synchronized

//如果使用synchronized关键字,这种方式在并发下并发量等同于1,如果频繁使用该对象,会大大降低性能
public static synchronized Lasy getInstance() {
    if (null == INSTANCE) {
        INSTANCE = new Lasy();
    }
    return INSTANCE;
}      

2.2 双重检测(DCL懒汉式单例)

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

// INSTANCE = new Lasy();
/** 以上操作并不是原子性操作,包括以下三个步骤:
  * 1. 分配内存空间
  * 2. 执行构造方法进行初始化
  * 3. 将对象指向这个空间
  */
/** 在多线程情况下可能出现指令重排,每个线程执行INSTANCE = new Lasy()这个操作的顺序可能不一致,那么就有可能出现线程A先执行了第一步和第三步,此时还未进行初始化,线程B执行判断语句null == INSTANCE时为false,将直接返回线程A未进行初始化的INSTANCE对象,所以要避免指令重排,使用volatile关键字。*/

private static volatile Lasy INSTANCE;

public static Lasy getInstance() {
    if (null == INSTANCE) {
        // 对象未被实例化,等待类对象锁
        synchronized (Lasy.class) {
            // 进入对象锁,其他线程等待,再次判断是否被实例化
            if (null == INSTANCE) {
                INSTANCE = new Lasy();
            }
        }
    }
    return INSTANCE;
}      

饿汉式和懒汉式比较:

①由于饿汉式是在类加载的时候进行实例化,线程安全;懒汉式是在第一次使用的时候进行实例化,此时可能多线程会同时进行实例化,所以线程不安全;

②饿汉式不支持延时加载,如果占用资源较多或者耗时较长,可能会影响启动时间;懒汉式支持延时加载,但是需要使用同步锁,如果这个类被频繁使用,可能导致性能问题。(相比较更加倾向于饿汉式单例,在启动时进行加载,如果遇到性能或者资源问题会提前暴露,相较于正在运行中出现问题更加可控)

3. 静态内部类

public class Test {
    private Test() {}

    private static class InnerTest{
        // final修饰变量是不可变的,初始化对象之后便不会再指向另一个对象
        private static final Test INSTANCE = new Test();
    }

    public static Test getInstance() {
        return InnerTest.INSTANCE;
    }
}      

但是在反射机制下,所有的私有方法都能被破解,如下实例表示通过静态内部类获得的实例对象和通过反射获得的holder实例对象并不是同一个,仍然破坏了单例

public static void main(String[] args) throws Exception {
    // 获取private的构造方法
    Constructor<Test> constructor = Test.class.getDeclaredConstructor(null);
    // 无视私有构造器
    constructor.setAccessible(true);
    Test test = constructor.newInstance();

    System.out.println(Test.getInstance().equals(test));
}

输出结果:false      

通过反射的源码可以看到,无法反射式创建枚举对象,所以我们可以考虑使用枚举来实现单例

单例模式

4. 枚举

public enum SingleEnum {
  INSTANCE;
}      

以下示例可见确实无法反射式创建枚举对象,当使用反射创建枚举对象时,会抛出异常信息Cannot reflectively create enum objects

public static void main(String[] args) throws Exception {
    // 获取private的构造方法(实际上枚举类的构造方法带有两个参数,并不是我们从源码看到的无参构造,需要使用源码工具来查看)
    Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(String.class, int.class);
    // 无视私有构造器
    constructor.setAccessible(true);
    SingleEnum single = constructor.newInstance();

    System.out.println(SingleEnum.INSTANCE.equals(single));
}      

输出:

单例模式