要不要来看看解决重排序问题后的单例模式

484 阅读4分钟

简单介绍

单例模式是最简单的设计模式之一,提供了一种创建对象的方式,确保在整个系统中只有一个对象被创建.单例模式解决了频繁创建重复对象的问题节约资源,可以省略创建对象所需要花费的时间,对于一些重量级对象而言这点是很重要的.并且因为不需要频繁创建对象 GC 的压力也会有所减轻.

单例模式的一些实现方式

通常来说在 Java 中的单例模式分为饿汉式和懒汉式,而且单例类需要一个 private 的构造函数防止被其他代码实例化.下面来具体说一下java 中单例模式的实现.

饿汉式

public class Singleton{
  	private static Singleton instance =new Singleton();
    //私有化构造方法
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

饿汉式的单例模式代码简单,线程安全.先创建对象,然后等待调用首先私有化构造方法,防止别人使用new 创建对象.通过classLoader机制保证了单例对象的唯一性 但是不能确保instance 是在调用getInstance()方法的时候生成的不能达到懒加载效果

懒汉式

public class Singleton{
    private static Singleton instance;
    private Singleton(){}
   	//加入 synchronize 保证线程安全
    public synchronized static Singleton getInstance(){
        if(instance==null){
            instance=new Singleton();
        }
        return instance;
    }
}

为了达到懒加载的效果,我们使用懒汉式的单例模式,在第一次调用方法getInstance()的时候才去创建对象.可以达到延迟加载的效果并且加入了 synchronize 保证线程安全,但每次调用代码的时候都要加锁,性能比较低还有可能发生阻塞

DCL双重校验锁

public class Singleton{
    //volatile防止指令重排序
    private static volatile Singleton instance=null;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if(instance==null){
            synchronized(Singleton.class){
              	//加入第二次校验
                if(instance==null){
                    instance=new Singleton();
                }
            }
        } 
        return instance;
    }
}

双重校验锁就是为了解决上述问题而存在的,先检查实例是否存在然后再去创建,可以不用每次调用方法都获取同步锁性能会有一些提升,减小的锁的颗粒度.但是 java 对象的创建和赋值不是一步操作的,有可能先去赋值给中instance之后才去创建 Singleton 这时添加volatile关键字防止指令重排序解决了这个问题.

对象创建的过程:

在代码的第 12 行 instance =new Singleton();大致分为三个过程 1.分配对象的内存空间 此时 instance !=null

2.初始化对象

3.将instance 指向分配的内存空间

其中2 和 3不一定是有序的 所以线程 B 会访问到一个还未初始化的对象

静态内部类

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

看过繁琐的DCL后 下面介绍一种简洁的单例模式静态内部类.当Singleton被创建的时候不会去加载SingleHoler,只有第一次调用getInstance()方法时才回去创建instance,加载SingleHoler将常量池中的符号引用替换成直接引用,这种方式不仅保证了线程安全而且可以达到延迟加载的效果.

classload机制

解决重排序的方法有两种,第一种就是使用 volatile ,第二种则是现在要介绍的方法

调用类的静态成员(非字符串常量)的时候会导致类(SingleHoler)的初始化.并且在执行类的初始化期间,JVM 会获取一个初始化锁,这个锁可以同步多个线程对同一个类的初始化.

类加载的步骤:

将符号引用替换成直接引用是在解析的阶段完成的.

最佳实践

public enum Singleton{
    INSTANCE;
    public void print(){
        System.out.println("快乐就完事了!");
    }
}

这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。 --《Effective Java 中文版 第二版》

简单到不能再简单了啊.jvm 在加载枚举类的时候会使用loadClass方法使用同步代码块解决线程安全问题.使用 enum 的单例模式还能避免反序列化破坏单例并且不能被反射攻击.