单例模式(多种写法)

什么是单例模式?

单例模式就是保证一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。

如何实现单例模式

因为保证一个类只能有一个实例,不能多次实例化,不能允许用户new对象,所以需要将构造方法私有化,通过提供类的方法来让外部获取对象实例
单例模式主要存在两种方式实现:饿汉式懒汉式

饿汉式

饿汉式即不管你需不要这个对象,先创建好,等你用的时候直接拿来用就行。
饿汉式代码实现:

class Hungry {
    private  static  Hungry instance = new Hungry();
    private Hungry(){
        
    }
    public static Hungry getInstance(){
        return instance;
    }

    public static void gethh(){
        System.out.println("hhh");
    }

}

饿汉式是线程安全的。

懒汉式

懒汉式即需要用到该实例的时候再去创建对象。

class Lazy {
    private  static  Lazy instance;
    private Lazy(){

    }
    public static Lazy getInstance(){
        if(instance == null){
            instance = new Lazy();
        }
        return instance;
    }

    public static void gethh(){
        System.out.println("hhh");
    }
}

该懒汉式代码在单线程下执行是没有问题的,但是在多线程下执行的话可能会因为并发问题导致实例化多次。因此可以对懒汉式加锁来控制类只允许被实例化一次。

懒汉式(加锁版)

class Lazy {
    private  static  Lazy instance;
    private Lazy(){

    }
    public  static Lazy getInstance(){
        synchronized (Lazy.class){
            if(instance == null){
                instance = new Lazy();
            }
        }

        return instance;
    }

    public static void gethh(){
        System.out.println("hhh");
    }
}

通过加锁保证了单例模式的安全性。但是锁的粒度太大,会严重影响性能。因此可以采用DCL双重检锁机制。

懒汉式(DCL版)

class Lazy {
    private  static  Lazy instance;
    private Lazy(){

    }
    public  static Lazy getInstance(){
        
        if(instance == null){
                synchronized (Lazy.class){
                    if(instance == null){
                        instance = new Lazy();
                    }                     
            }
        }

        return instance;
    }

    public static void gethh(){
        System.out.println("hhh");
    }
}

这样的话就是只有当对象未创建的时候才请求加锁,对象创建以后都不会再去获取锁加锁。这样就可以提高程序效率,并保证实例只有一个。但这段代码依然存在问题,因为JVM是存在指令重排的,所以在多线程下不一定是线程安全的。原因是当某一个线程第一次检测的时候,读取到instance不为空的话,instance的引用对象可能没有完成实例化。因为 instance = new Lazy();可以分为以下三步进行完成:

  • memory = allocate(); // 1、分配对象内存空间
  • instance(memory); // 2、初始化对象
  • instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
    因为2和3是不存在依赖关系的,所以可能会发生指令重排,在单线程下是完全没有问题的,但是多线程下就需要禁止重排。
  • memory = allocate(); // 1、分配对象内存空间
  • instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
  • instance(memory); // 2、初始化对象

当我们执行到重排后的步骤2,试图获取instance的时候,会得到null,因为对象的初始化还没有完成,而是在重排后的步骤3才完成,因此执行单例模式的代码时候,就会重新在创建一个instance实例。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题。指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性
解决以上问题便可以使用volatile关键字,来禁止指令重排,保证单例模式的线程安全性。
最终代码为:

懒汉式(最终版)

class Lazy {
    private volatile   static  Lazy instance;
    private Lazy(){

    }
    public  static Lazy getInstance(){

        if(instance == null){
                synchronized (Lazy.class){
                    if(instance == null){
                        instance = new Lazy();
                    }
            }
        }

        return instance;
    }

    public static void gethh(){
        System.out.println("hhh");
    }
}
posted @ 2022-04-16 21:58  YoungerWb  阅读(50)  评论(0编辑  收藏  举报