单例设计模式分为两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
饿汉式
public class Singleton01 { //饿汉式,不用考虑线程安全问题
//私有化构造方法
private Singleton01(){}
private static Singleton01 instance;
//初始化静态变量
static {
instance = new Singleton01();
}
//获取实例
public static Singleton01 getInstance() {
return instance;
}
}
该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。
instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
懒汉式
public class Singleton02 { //懒汉式,需要考虑线程安全问题
//私有化构造方法
private Singleton02(){}
//使用volatile保证可见性和禁止指令重排序
private static volatile Singleton02 instance= new Singleton02();
//双重检查锁
public static Singleton02 getInstance() {
if(instance==null) //读操作不需要获取锁,直接返回
{
synchronized (Singleton02.class)
{
if(instance==null)
{
instance = new Singleton02();
}
}
}
return instance;
}
}
因为懒汉式采用的是懒加载模式,在调用 getInstance( ) 方法时才会实例化 instance 对象,因此在多线程环境下可能出现线程安全问题。
这里使用了双重检查锁来解决懒汉式的线程安全问题,并使用volatile关键字禁止指令重排序来防止可能出现的空指针问题。
Question 01:为什么不直接使用 synchronized 关键字修饰整个 getInstance( ) 方法
对于 getInstance( ) 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没有必要让每个线程必须持有锁才能调用该方法,这样会严重影响性能。
Question 02:volatile 关键字起到了什么作用?
在这里 volatile 关键字是用来保证可见性和防止指令重排序。
保证可见性:
在多线程环境下,每个线程都有自己的工作内存,线程在操作共享变量时,会先将变量从主内存复制到自己的工作内存中,对变量的操作都在工作内存中进行,操作完成后再将变量的值刷新回主内存。如果一个线程修改了共享变量的值,但没有及时刷新回主内存,或者其他线程没有及时从主内存中读取最新的值,就会导致数据不一致的问题。
volatile 关键字可以保证被修饰的变量在被修改后立即刷新到主内存中,同时其他线程在使用该变量时会直接从主内存中读取最新的值,从而保证了变量在多个线程之间的可见性。
防止指令重排序导致可能的空指针问题
Java 编译器和处理器为了提高性能,可能会对指令进行重排序,即在不影响单线程程序执行结果的前提下,对代码的执行顺序进行调整。但是,在多线程环境下,指令重排序可能会导致程序出现错误。
volatile 关键字可以禁止编译器和处理器对指令进行重排序,保证代码的执行顺序与编写顺序一致。
在上面的代码里:instance = new Singleton( ); 这行代码在实际执行时会分为三个步骤:
为 Singleton 对象分配内存空间。
初始化 Singleton 对象。
将 instance 引用指向分配的内存空间。
在没有使用 volatile 关键字的情况下,编译器和处理器可能会对这三个步骤进行重排序,例如先执行步骤 3,再执行步骤 2。在多线程环境下,如果一个线程在执行步骤 3 后,另一个线程判断 instance 不为 null 并直接使用该实例,此时 Singleton 对象却没有被初始化,从而导致返回了一个没有初始化的instance对象。而使用 volatile 关键字修饰 instance 变量后,可以禁止指令重排序,保证这三个步骤按照顺序执行,从而避免了上述问题。