Java 设计模式 -- 单例模式的实现(饿汉式枚举饿汉式懒汉式双检锁懒汉式内部类懒汉式)jdk 中用到单例模式的场景DCL实现单例需用volatile 修饰静态变量

Posted CodeJiao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 设计模式 -- 单例模式的实现(饿汉式枚举饿汉式懒汉式双检锁懒汉式内部类懒汉式)jdk 中用到单例模式的场景DCL实现单例需用volatile 修饰静态变量相关的知识,希望对你有一定的参考价值。

文章目录


1. 单例模式

要求

  • 掌握五种单例模式的实现方式
  • 理解为何 DCL(Double Check Lock, 双检锁机制) 实现单例时要使用 volatile 修饰静态变量
  • 了解 jdk 中用到单例的场景

1.1 何为单例模式

  • 单例模式保证java应用程序中,一个类Class只有一个实例在,使用单例模式好处在于可以节省内存,节约资源,对于一般频繁创建和销毁对象的可以使用单例模式
  • 因为它限制了实例的个数,有利于java垃圾回收。好的单例模式也能提高性能。例如:数据库连接池、httpclient连接单例。
  • 对于系统中的某些类来说,只有一个实例很重要,Windows中就只能打开一个任务管理器。

1.2 单例模式的实现(饿汉式 & 懒汉式)

单例设计模式分类两种

  • 饿汉式:类加载就会导致该单实例对象被创建。
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。

2. 单例模式的实现


2.1 饿汉式

public class Singleton1 implements Serializable 
    private Singleton1() 
        if (INSTANCE != null) 
            throw new RuntimeException("单例对象不能重复创建");
        
    

    private static final Singleton1 INSTANCE = new Singleton1();

    public static Singleton1 getInstance() 
        return INSTANCE;
    

    public Singleton1 readResolve() 
        return INSTANCE;
    

  • 构造方法抛出异常是防止反射破坏单例
  • readResolve() 是防止反序列化破坏单例

2.2 枚举饿汉式

public enum Singleton2 
    INSTANCE;

    private Singleton2() 
    

    public static Singleton2 getInstance() 
        return INSTANCE;
    

  • 枚举饿汉式能天然防止反射、反序列化破坏单例

2.3 懒汉式

public class Singleton3 implements Serializable 
    private Singleton3() 
    

    private static Singleton3 INSTANCE = null;

    // 锁住的是 Singleton3.class 字节码对象
    public static synchronized Singleton3 getInstance() 
        if (INSTANCE == null) 
            INSTANCE = new Singleton3();
        
        return INSTANCE;
    

  • 其实只有首次创建单例对象时才需要同步,但该代码实际上每次调用都会同步。因此有了下面的双检锁改进。

2.4 双检锁懒汉式 DCL(Double Check Lock, 双检锁机制)

public class Singleton4 implements Serializable 
    private Singleton4() 
    

    private static volatile Singleton4 INSTANCE = null; // 可见性,有序性

    public static Singleton4 getInstance() 
        if (INSTANCE == null) 
            synchronized (Singleton4.class) 
                if (INSTANCE == null) 
                    INSTANCE = new Singleton4();
                
            
        
        return INSTANCE;
    


2.4.1 为何必须加 volatile

  • INSTANCE = new Singleton4() 不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值。

    其中后两步可能被指令重排序优化,变成先赋值、再调用构造。
  • 如果线程1 先执行了赋值,线程2 执行到第一个 INSTANCE == null 时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象。

2.4.2 说明:内部第二次判断 INSTANCE == null 的原因

        if (INSTANCE == null) 
            synchronized (Singleton4.class) 
                if (INSTANCE == null) 
                    INSTANCE = new Singleton4();
                
            
        
  1. 刚开始INSTANCE为NULL,线程1和线程2都打算去竞争synchronized锁。

  2. 假如现在是线程2竞争锁成功。

  3. 线程2执行了赋值语句,成功解锁,这时线程1得到了synchronized锁。

  4. 然后线程1成功把线程2赋值的值给覆盖了。


2.4.3 volatile指令的作用

volatile可以保证共享变量的可见性和有序性。

保证可见性:

  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) 
 num = 2;
 ready = true; // ready 是 volatile 赋值带写屏障
 // 写屏障

  • 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) 
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) 
 r.r1 = num + num;
  else 
 r.r1 = 1;
 


保证有序性:

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r) 
 num = 2;
 ready = true; // ready 是 volatile 赋值带写屏障
 // 写屏障

  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r) 
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) 
 r.r1 = num + num;
  else 
 r.r1 = 1;
 

volatile 不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序

2.5 内部类懒汉式

public class Singleton5 implements Serializable 
    private Singleton5() 
    

    private static class Holder 
        static Singleton5 INSTANCE = new Singleton5();
    

    public static Singleton5 getInstance() 
        return Holder.INSTANCE;
    

  • 避免了双检锁的缺点(加了static关键字的代码可以理解为静态代码块的代码,而静态代码块的代码不需要去考虑线程安全,JVM在处理静态代码块时是线程安全的)

3. jdk 中用到单例模式的场景

使用单例模式大部分都是在别的库里面使用了单例模式,你的项目中不要乱用单例模式,很容易用错。所以面试官如果问你在哪里用到了单例模式,最好不要说是在项目当中用到了单例模式。我们可以去列举 jdk 中用到单例模式的场景。因为jdk是写好的成熟的库,就算真的有一点小的问题,面试官也挑不出什么毛病。

  • Runtime 体现了饿汉式单例

    我们可以看出,System类的exit方法和gc方法都是调用的Runtime类的方法。

  • Console 体现了双检锁懒汉式单例


4. 反序列化破坏单例和反射破坏单例代码实现

    private static void serializable(Object instance) throws IOException, ClassNotFoundException 
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(instance);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        System.out.println("反序列化创建实例:" + ois.readObject());
    

    private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException 
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        System.out.println("反射创建实例:" + constructor.newInstance());
    


以上是关于Java 设计模式 -- 单例模式的实现(饿汉式枚举饿汉式懒汉式双检锁懒汉式内部类懒汉式)jdk 中用到单例模式的场景DCL实现单例需用volatile 修饰静态变量的主要内容,如果未能解决你的问题,请参考以下文章

设计模式之单例模式(Java实现)

Java 设计模式----单例模式--饿汉式

java设计模式之单例模式你真的会了吗?(饿汉式篇)

Java单例模式《一》饿汉式

关于Java单例模式中懒汉式和饿汉式的两种类创建方法

Java之单例设计模式