单例模式的四种具体写法

发布时间:2023年12月24日

1.单例模式的介绍

1.1概念

单例模式就是保证对象只被创建一次,并在整个程序中复用。

1.2如何实现单例

  • 将构造方法私有化
  • 提供一个全局唯一获取该类实例的方法帮助用户获取该类的实例

1.3应用场景

主要被用于一个全局类的对象在多个地方被使用并且对象的状态是全局变化的场景下。

1.4优点

单例模式保障了整个系统只有一个对象能被使用,很好地节约了资源。

1.5单例模式的写法

  • 饿汉模式
  • 懒汉模式
  • 静态内部类
  • 双重校验锁

2、四种模式的使用

2.1饿汉模式

饿汉模式就是在加载类(比如下面加载Singleton)的时候就直接new一个对象,然后在方法中直接将对象返回给用户

public class Singleton {
    // 使用static修饰,类加载的时候new一个对象
  	private static Singleton INSTANCE = new Singleton();
  
  	// 构造器私有化
  	private Singleton() {}
  	
  	public static Singleton getInstance() {
      	return INSTANCE;
    }
}

2.2懒汉模式

懒汉模式就是在加载类的时候只声明变量,不new对象,后面需要用到的时候再new对象,并把对象赋给变量

public class Singleton {
    
  	private static Singleton INSTANCE;
  
  	// 构造器私有化
  	private Singleton() {}
  	
  	public static Singleton getInstance() {
      	if (INSTANCE == null) {
          	INSTANCE = new Singleton();
        }
      	return INSTANCE;
    }
}
饿汉模式与懒汉模式的区别

饿汉模式在getInstance方法调用前实例已经被创建,因此实例在类加载的时候就已经存在于JVM中,因此饿汉模式是线程安全的,而懒汉模式则是在调用getInstance方法后才创建实例,因此线程是不安全的

2.3静态内部类

通过在类中定义一个静态内部类,将对象实例的创建与初始化放在内部类中完成,我们在getInstance中获取对象直接通过静态内部类调用单例对象

正是因为类的静态内部类在JVM中的唯一性才保证了单例对象的唯一性,从而静态内部类同样是线程安全的

public class Singleton {
  
  	private static class SingletonHolder {
      	private static final Singleton INSTANCE = new Singleton();
    }
  
  	private Singleton(){}
  
  	public static final Singleton getInstance(){
      	return SingletonHolder.INSTANCE;
    }
}

2.4双重校验锁

普通的懒汉模式在单线程场景下是线程安全的,但在多线程场景下是非线程安全的。

先来看看普通的懒汉模式

public class Singleton {
    
  	private static Singleton INSTANCE;
  
  	private Singleton() {}
  	
  	public static Singleton getInstance() {
      	if (INSTANCE == null) {
          	INSTANCE = new Singleton();
        }
      	return INSTANCE;
    }
}

在多线程同时调用getInstance方法时,由于方法没有加锁,可能会出现以下情况:

  1. ?这些线程可能会创建多个对象
  2. 某个线程可能会得到一个未完全初始化的对象

对于1的解释

public static Singleton getInstance() {
    if (INSTANCE == null) {
        /**
         * 由于没有加锁,当线程A刚执行完if判断INSTANCE为null后还没来得及执行INSTANCE = new Singleton()
         * 此时线程B进来,if判断后INSTANCE为null,且执行完INSTANCE = new Singleton()
         * 然后,线程A接着执行,由于之前if判断INSTANCE为null,于是执行INSTANCE = new Singleton()重复创建了对象
         */
        INSTANCE = new Singleton();
    }
    return INSTANCE;
}

对于2的解释

public static Singleton getInstance() {
    if (INSTANCE == null) {
        /**
         * 由于没有加锁,当线程A刚执行完if判断INSTANCE为null后开始执行 INSTANCE = new Singleton()
         * 但是注意,new Singleton()这个操作在JVM层面不是一个原子操作
         *
         *(具体由三步组成:1.为INSTANCE分配内存空间;2.初始化INSTANCE;3.将INSTANCE指向分配的内存空间,
         * 且这三步在JVM层面有可能发生指令重排,导致实际执行顺序可能为1-3-2)
         *
         * 因为new操作不是原子化操作,因此,可能会出现线程A执行new Singleton()时发生指令重排的情况,
         * 导致实际执行顺序变为1-3-2,当执行完1-3还没来及执行2时(虽然还没执行2,但是对象的引用已经有了,
         * 只不过引用的是一个还没初始化的对象),此时线程B进来进行if判断后INSTANCE不为null,
         * 然后直接把线程A new到一半的对象返回了
         */
        INSTANCE = new Singleton();
    }
    return INSTANCE;
}

因此通过单例模式的双重检验锁模式去解决以上问题

public class Lock2Singleton {
  	private volatile static Lock2Singleton INSTANCE;    // 加 volatile
  
  	private Lock2Singleton() {}
  
  	public static Lock2Singleton getSingleton() {
      	if (INSTANCE == null) {                         // 双重校验:第一次校验
          	synchronized(Lock2Singleton.class) {        // 加 synchronized
              	if (INSTANCE == null) {                 // 双重校验:第二次校验
                  	INSTANCE = new Lock2Singleton();
                }
            }
        }
      	return INSTANCE;
    }
}

为啥要双重校验?

第一次校验是为了提高效率,避免INSTANCE不为null时仍然去竞争锁

第二次校验是为了避免多个线程重复创建对象

为啥要加volatitle?

加volatitle是为了禁止指令重排,避免某个线程可能会得到一个未完全初始化的对象

具体执行过程:

  1. 判断 INSTANCE 是否为null,检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量;
  2. 不为null,直接返回,不用去竞争锁,为null,获取锁,然后再次判断是否为null,
  3. 为null,创建并返回,不为null直接返回
文章来源:https://blog.csdn.net/Mr_liu888/article/details/135154564
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。