手把手教你写单例模式的几种写法

目录

0 什么是单例模式

1 饿汉法

2 懒汉法

3 双重校验锁

3.1 volatile关键字的作用及原理

4 枚举法 (Effective Java推荐)

5 总结

本文旨在学习总结不同的单例模式写法,并做优缺点分析。

0 什么是单例模式

在《Design Patterns:Elements of Resuable Object-Oriented Software》中的定义是:Ensure a class only has one instance,and provide a global point of access to。它的主要特点不是根据客户程序调用生成一个新的实例,而是控制某个类型的实例数量——唯一一个(《设计模式-基于C#的工程化实现及扩展》,王翔)。也就是说,单例模式就是保证在整个应用程序的生命周期中,在任何时刻,被指定的类只有一个实例,并为客户程序提供一个获取该实例的全局访问点。

 

1 饿汉法

在第一次引用该类的时候就创建对象。在类加载的时候就已经创建好了一个静态的对象Singleton供系统使用,以后不再改变,所以它是线程安全的,避免了多线程同步的问题。

缺点:即使单例没有用到,也会被创建,浪费内存资源。

public class Singleton {   
    private static Singleton = new Singleton();
    private Singleton() {}
    public static getSignleton(){
        return singleton;
    }
}

2 懒汉法

懒汉法解决了饿汉法浪费资源的问题,在需要的时候才去创建对象。

缺点:线程不安全。 在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例。

竞态条件会导致singleton引用被多次赋值,使用户得到两个不同的单例。

public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton() {
        if(singleton == null)    // 并发场景下,这里存在竞态条件
           { singleton = new Singleton();}
        return singleton;
    }
}

3 双重校验锁

考虑线程安全和效率,修改以下3点。这种方法之所以成为是双重校验锁,就是因为在getSingleton() 方法中,进行了两次null的检查。

public class Singleton {
    private static volatile Singleton singleton = null;  //1.volatile保证对线程的可见性并禁止指令重排优化
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        if(singleton == null){   //2.提高效率,减少synchronized加锁操作
            synchronized (Singleton.class){  //3.对象锁,阻塞
                if(singleton == null){ 
                    singleton = new Singleton();   //不加volatile关键字多线程情况下用户会拿到半个对象
                }
            }
        }
        return singleton;
    }    
}

3.1 volatile关键字的作用及原理

双重校验锁方法修改的2、3点很好理解。那么第1点,如果没有volatile 关键字,会出现什么问题呢? 

我们先来看volatile关键字的作用:(1)保持内存可见性;(2)防止指令重排。

内存可见性:所有线程都能看到共享内存的最新状态。

我们来看这样一个可变整数类:

public class MutableInteger {
    private int value;
    public int get(){
        return value;
    }
    public void set(int value){
        this.value = value;
    }
}

类MutableInteger 不是线程安全的,在多线程的情况下,如果线程1调用了set方法,那么正在调用get的线程2 可能看到更新后的值,也可能看不到。解决方法就是,将value 声明成 volatile 变量。

private volatile int value;

防止指令重排:在Java中看似顺序的代码在JVM中,可能会出现编译器或者CPU对这些操作指令进行了重新排序。

接下来,我们来分析一下,如果没有volatile 关键字,会出现什么问题呢? 

问题出在这行简单的赋值语句:

singleton = new Singleton();

它并不是一个原子操作。事实上,它可以”抽象“为下面几条JVM指令:

memory = allocate();     //1:分配对象的内存空间
initInstance(memory);    //2:初始化对象
singleton = memory;      //3:设置singleton指向刚分配的内存地址

操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:

memory = allocate();    //1:分配对象的内存空间
singleton = memory;     //3:设置singleton指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory);   //2:初始化对象

可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用 singleton 指向内存 memory 时,这段崭新的内存还没有初始化——即,引用singleton指向了一个"被部分初始化的对象"。此时,如果另一个线程调用getInstance方法,由于singleton已经指向了一块内存空间,从而if条件判为false,方法返回singleton 引用,用户得到了没有完成初始化的“半个”单例对象。

所以,解决方法,就是将singleton 声明为volatile变量。

 

但是,上面提到的所有实现方式都有两个共同的缺点:

  • 都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。
  • 可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

4 枚举法 (Effective Java推荐)

public enum Singleton {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

5 总结

无论以上哪种方法都需要考虑的是:一个核心原理就是私有构造,并且通过静态方法获取一个实例。

在这个过程中还需要考虑:

  • 线程安全
  • 延迟加载
  • 序列化和反序列化安全

 

以上?

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(JAVA核心技术系列)