设计模式是每个程序员在工作和求职中都涉及的区域,想要真正掌握好设计模式的精髓并运用到工作中去,却并不容易,本文将针对单例模式模式中的双重检查锁模式进行详细讲解,并顺便介绍下其他的单例模式。
概念
所谓单例,就是整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。在Java,一般常用在工具类的实现或创建对象需要消耗资源。
单例模式的特点
- 类构造器私有
- 持有自己类型的属性
- 对外提供获取实例的静态方法
单例的各种实现方式
- 双重检查模式
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
双重检查的作用:
第一重检查:减少不必要的上锁,将锁的粒度细化,避免不必要的上锁,使得最外层所有的人都得排队等待,导致资源浪费;即先判断对象有没有实例化,有的话直接取值就行,没有实例化的则进行加锁实例化;
第二重检查:在多线程的情况下,有可能已经有之前获取过锁的线程已经实例化了该对象,为了防止多次实例化,此处需要进行第二次检查;假如有A、B两个线程,都通过了第一层检查,然后A先获取锁,实例化对象后释放锁,此时B获取到锁,这个时候如果不去进行第二重检查,则B也会实例化对象,并把A的给覆盖掉;
指令重排的概念: 一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息,
int a = 10; //执行顺序1
int b = 20; //执行顺序2
int c = a + b; //执行顺序3
理论上,虚拟机在执行代码的时候,会按照上面的顺序1-》2-》3执行,但是编译器可能会进行相应的优化导致执行顺序变成2-》1-》3,这个操作就叫指令重排。虽然执行结果依然是正确的,但是在多线程的情况下这种指令重排可能会造成灾难性的影响。
首先我们先看一下new Object()底层实现的步骤:
public class Main {
public static void main(String[] args) {
CarServiceImpl carService = new CarServiceImpl();
}
}
// 对应的字节码如下:
public class com.spring.learn.Main {
public com.spring.learn.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
// 1、创建对象实例,并分配内存
0: new #2 // class com/spring/learn/service/impl/CarServiceImpl
// 复制栈顶地址,并将其压入栈顶
3: dup
// 2、调用构造器方法,初始化对象
4: invokespecial #3 // Method com/spring/learn/service/impl/CarServiceImpl."<init>":()V
// 3、存入局部方法变量表
7: astore_1
8: return
}
从字节码可以看到创建一个对象实例,可以分为三步:
- 1.分配对象内存;
- 2.调用构造器方法,执行初始化;
- 3.将对象引用赋值给变量;
虚拟机实际运行时,以上指令可能发生重排序。以上步骤2、3可能发生重排序,但是并不会重排序1的顺序。也就是说1这个指令都需要先执行,因为2、3指令需要依赖1指令执行结果。
在上述双重检查锁的示例代码中,如果线程ThreadA获取到锁,并在创建实例的时候发生了指令重排序,即先执行步骤3-》再执行步骤2,在执行步骤3的时候,此时ThreadB在最外层检查的时候发现对象不为null,然后直接拿到一个还未初始化的对象,造成ThreadB线程访问时发生异常。
volatile的作用:
- 保证可见性。使用 volatile定义的变量,将会保证对所有线程的可见性;
- 禁止指令重排序优化。由于volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
使用局部变量优化
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
Singleton singletonLocal = this.singleton;
if (singletonLocal == null) {
synchronized (Singleton.class) {
if (singletonLocal == null) {
singletonLocal = new Singleton();
this.singleton = singletonLocal
}
}
}
return this.singleton;
}
}
可以看到方法内部使用局部变量,首先将实例变量值赋值给该局部变量,然后再进行判断。最后内容先写入局部变量,然后再将局部变量赋值给实例变量。使用局部变量相对于不使用局部变量,可以提高性能。主要是由于volatile变量创建对象时需要禁止指令重排序,这就需要一些额外的操作,而局部变量的话,线程安全的,可以节省这些额外的操作。
- 懒汉模式
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 = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
优点:线程安全。
缺点:程序启动的时候就初始化,容易产生垃圾,和资源浪费。
- 静态内部类单例模式
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return Inner.instance;
}
private static class Inner {
private static final Singleton instance = new Singleton();
}
}
只有第一次调用getInstance方法时,虚拟机才加载Inner并初始化instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。