单例设计模式

单例模式

单 :唯一 例:实例
某个类在整个系统只有一个实例对象,可以获取和使用的代码模式;

要点:

  • 某个类只能有一个实例

    • 构造器私有化
  • 必须自行创建这个实例

    • 含有一个该类的静态变量来保存这个唯一实例
  • 对外提供获取该实例对象方式

    1. 直接暴露
    2. 用静态变量get方法获取
  • 饿汉式:直接创建对象,不存在线程安全问题

  1. 直接实例化饿汉式(简洁直观)
  2. 枚举式(最简洁)
  3. 静态代码块饿汉式(适合复杂实例化)
  • 懒汉式:延迟创建对象
  1. 线程不安全(适用于单线程)
  2. 线程安全(适合于多线程)
  3. 静态内部类形式(适用于多线程)

饿汉式直接创建

直接创建实例对象,不管是否需要直接创建

  1. 构造器私有化
  2. 自行创建,并且静态变量保存
  3. 向外提供这个实例
  4. 强调单例,用fina修改
public class Singleton {

	/**
	 * 该函数限制用户主动创建实例
	 */
	private Singleton() {}

	private static final Singleton singleton = new Singleton();

	/**
	 * 获取Singleton实例,也叫静态工厂方法
	 * @return Singleton
	 */
	public static Singleton getInstance() {
		return singleton;
	}
}

适用静态代码块

public class  Singleton{
   public static final Singleton INSTANCE;
   static {
       INSTANCE = new Singleton();
   }
  private Singleton(){}
}

枚举
枚举类型,表示该类型的对象是有限几个
可以限定一个,成就单列

public enum  Singleton2{
    INSTANCE
}

懒汉式

延迟创建这个实例对象

  1. 构造器私有化
  2. 用一个静态变量保存这个唯一实例
  3. 提供一个静态方法,获取这个实例对象
public class Singleton {
    private static Singleton instance;
    private Singleton(){
        
    }
    public static Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

多线程不安全,加锁

public class Singleton {
    private static Singleton instance;
    private Singleton(){

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

上面方式解决了线程问题,但是出现了并发性能问题,
比较差,应该做些优化,如果没有实例对象进行加锁创建,如果已经有了不需要加锁,直接获取实例。

双重检查 + lock方式

public class Singleton {
    private static Singleton instance;
    private Singleton(){

    }
    public static Singleton getInstance(){
        if (instance == null) {  //线程A和线程B同时看到 instance不为空,直接返回对象
            synchronized (Singleton.class) { //AB获取锁
                if (instance == null) {  //其中一个进入内部,另一个阻塞
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

执行流程

  • 第一个判断 ,如果instance不为空,则直接返回对象,不需要获取锁:而如果多线程发现instance为空,进入下一步,获取锁
  • 多线程抢锁,只有一个线程成功,再次判断是否为空(因为可能被之前线程实例化了),再看看创建对象
  • 其它线程看到不为空后,就不会获取锁了

引发另一个问题: 指令重排
使用volatile防止指令重排创建一个对象,在JVM中简化为三步过程:

  • 为singleton分配内存空间
  • 初始化singleton对象
  • 让引用指向该分配好的位置

JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。

使用volatile关键字可以防止指令重排序使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生NPE异常了

volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。


public class Singleton {
    private static volatile Singleton instance;
    private Singleton(){

    }
    public static Singleton getInstance(){
        if (instance == null) {  //线程A和线程B同时看到 instance不为空,直接返回对象
            synchronized (Singleton.class) { //AB获取锁
                if (instance == null) {  //其中一个进入内部,另一个阻塞
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

静态内部类方式 ,

保持多线程安全,和延迟加载效果

在内部类被加载和初始化时,才会创建instance实例对象,静态内部类不会自动随着外部类的加载和初始化而初始化,单独的去加载和初始化。因为内部类加载和初始化时,创建它,线程安全。

public class Singleton {
    
    private Singleton(){

    }
    private static class Inner{
        private static final Singleton instance = new Singleton();
    }
    public static Singleton getInstance(){
        return Inner.instance;
    }
}
posted @ 2021-04-29 10:46  杰的博客#  阅读(54)  评论(1编辑  收藏  举报