Java 单例模式浅析

前言

文章目录

  • 前言
  • 单例模式
    • 单例模式介绍
    • 单例模式的应用
  • 单例模式的浅析
    • 饿汉式
    • 懒汉式——单线程
    • 懒汉式——多线程
    • 懒汉式——双重校验锁
      • 反编译代码
      • 分析原因
    • 懒汉式——线程安全双重校验锁
    • 静态内部类
  • 附录

单例模式

这篇文章主要着重介绍单例模式的优缺点并做分析,如何使用更优的单例模式。

单例模式介绍

维基百科:单例模式

单例模式,也叫单子模式,是一种常用的软件设计模式,属于创建型模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。

单例模式的应用

许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

单例模式的浅析

饿汉式

public class EagerSingleton {
    // 由于这个实例在Application创建的时候就会被创建,不管这个实例是否会被使用。
    // 这可能会导致内存泄露的问题。
    private static final EagerSingleton instance = new EagerSingleton();

    private EagerSingleton() {
    }

    /**
     * 饿汉模式
     */
    public static EagerSingleton getInstance() {
        return instance;
    }
}
  • 优点:第一次加载类的时候就会被创建,不需要考虑线程安全的问题。
  • 缺点:不管是否使用,都会被创建。可能导致内存泄露。

懒汉式——单线程

public class LazySingleton1 {
    private static LazySingleton1 instance = null;

    private LazySingleton1() {
    }

    /**
     * 仅仅适用于单线程环境,不能保证线程安全
     */
    public static LazySingleton1 getInstance() {
        if (null == instance) {
            instance = new LazySingleton1();
        }
        return instance;
    }
}
  • 缺点:只适用于单线程环境,不能保证线程安全。

懒汉式——多线程

public class LazySingleton2 {
    private static LazySingleton2 instance = null;

    private LazySingleton2() {
    }

    /**
     * 适用于多线程环境,但是由于加了方法锁,强行将并行变串行,效率低下。
     */
    public static synchronized LazySingleton2 getInstance() {
        if (instance == null) {
            instance = new LazySingleton2();
        }
        return instance;
    }
}
  • 缺点:
    • 适用于多线程环境,但是由于加了方法锁,强行将并行变串行。每次getInstance都需要加锁,效率低下。
    • ……

懒汉式——双重校验锁

public class LazySingleton3 {
    // 这一步不是线程安全的
    private static LazySingleton3 instance = null;

    private LazySingleton3() {
    }

    /**
     * 双重检查加锁(推荐);问题:不能彻底保证线程安全。
     */
    public static synchronized LazySingleton3 getInstance() {
        // 先判断实例是否存在,若不存在再对类对象进行加锁处理
        if (instance == null) {
            // 类锁,确保创建唯一的对象
            synchronized (LazySingleton3.class) {
                if (instance == null) {
                    instance = new LazySingleton3();
                }
            }
        }
        return instance;
    }
}
  • 缺点:使用双重校验锁可能会返回一个不正确的对象。

Partially created objects can be returned by the Double Checked Locking pattern when used in Java. An optimizing JRE may assign a reference to the baz variable before it calls the constructor of the object the reference points to.


Note: With Java 5, you can make Double checked locking work, if you declare the variable to be volatile.

反编译代码

➜  Singleton git:(master) ✗ javap -c -s -l LazySingleton3.class 
Compiled from "LazySingleton3.java"
public class Practice.Java.Singleton.LazySingleton3 {
  public static synchronized Practice.Java.Singleton.LazySingleton3 getInstance();
    descriptor: ()LPractice/Java/Singleton/LazySingleton3;
    Code:
       0: getstatic     #2                  // Field instance:LPractice/Java/Singleton/LazySingleton3;
       3: ifnonnull     37
       6: ldc           #3                  // class Practice/Java/Singleton/LazySingleton3
       8: dup
       9: astore_0
      10: monitorenter
      11: getstatic     #2                  // Field instance:LPractice/Java/Singleton/LazySingleton3;
      14: ifnonnull     27
      17: new           #3                  // class Practice/Java/Singleton/LazySingleton3
      20: dup
      21: invokespecial #4                  // Method "":()V
      24: putstatic     #2                  // Field instance:LPractice/Java/Singleton/LazySingleton3;
      27: aload_0
      28: monitorexit
      29: goto          37
      32: astore_1
      33: aload_0
      34: monitorexit
      35: aload_1
      36: athrow
      37: getstatic     #2                  // Field instance:LPractice/Java/Singleton/LazySingleton3;
      40: areturn
    Exception table:
       from    to  target type
          11    29    32   any
          32    35    32   any
    LineNumberTable:
      line 15: 0
      line 17: 6
      line 18: 11
      line 19: 17
      line 21: 27
      line 23: 37

  static {};
    descriptor: ()V
    Code:
       0: aconst_null
       1: putstatic     #2                  // Field instance:LPractice/Java/Singleton/LazySingleton3;
       4: return
    LineNumberTable:
      line 5: 0
}
  • 核心代码:
      // 开辟一块内存区域,作为对象的空间
      17: new           #3                  // class Practice/Java/Singleton/LazySingleton3
      // 复制操作数栈顶值,并将其压入栈顶
      20: dup
      // 执行对象的初始化操作
      21: invokespecial #4                  // Method "":()V
      // 给类的静态字段赋值
      24: putstatic     #2                  // Field instance:LPractice/Java/Singleton/LazySingleton3;

分析原因

上述核心代码中,21行和24行的代码可能由于指令重排,导致getInstance出现错误。

以下摘录自:JVM 指令重排对双重校验锁单例模式的影响

memory = allocate(); 	//1:分配对象的内存空间
ctorInstance(memory); 	//2:初始化对象
instance = memory; 		//3:设置 instance 指向刚分配的内存地址

但是经过重排序后如下:

memory = allocate(); 	//1:分配对象的内存空间
instance = memory; 		//3:设置 instance 指向刚分配的内存地址,此时对象还没被初始化
ctorInstance(memory); 	//2:初始化对象

可以看到指令重排之后,instance 指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。在线程 A 初始化完成这段内存之前,线程 B 虽然进不去同步代码块,但是在同步代码块之前的判断就会发现 instance 不为空,此时线程 B 获得 instance 对象进行使用就可能发生错误。

  • 还有一篇关于指令重排的讨论,建议看看:指令重排是否会导致map put操作时,其他线程取到空的问题

懒汉式——线程安全双重校验锁

public class LazySingletonDoubleCheck {

    // 单单的static并不能保证线程安全,应该加上volatile
    private static volatile LazySingletonDoubleCheck instance = null;

    private LazySingletonDoubleCheck() {
    }

    /**
     * 双重检查加锁(推荐)
     */
    public static synchronized LazySingletonDoubleCheck getInstance() {
        // 先判断实例是否存在,若不存在再对类对象进行加锁处理
        if (instance == null) {
            synchronized (LazySingletonDoubleCheck.class) {
                if (instance == null) {
                    instance = new LazySingletonDoubleCheck();
                }
            }
        }
        return instance;
    }
}

根据上述分析,给出如上的单例模式用法。

静态内部类

public class StaticSingleton {

    private StaticSingleton() {

    }

    /**
     * 静态内部类(推荐)
     */
    public static StaticSingleton getInstance() {
        return SingletonHolder.instance;
    }
    
    private static class SingletonHolder {
        private static final StaticSingleton instance = new StaticSingleton();
    }
}
  • 优点:
    • 外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。
    • 第一次调用getInstance()方法会导致虚拟机加载SingletonHolder类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
  • 缺点:
    • 由于是静态内部类的形式去创建单例的,故外部无法传递参数进去。

静态内部类instance在创建过程中又是如何保证线程安全的呢?——《深入理解JAVA虚拟机》

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。

附录

  • How to make the perfect Singleton?
  • 深入理解单例模式:静态内部类单例原理
  • Java实现单例模式(懒汉式、饿汉式、双重检验锁、静态内部类方式、枚举方式)
  • volatile底层原理详解
  • JVM 指令重排对双重校验锁单例模式的影响
  • 单例模式–双重检验锁真的线程安全吗
  • 双重检查锁定与单例模式真的线程安全吗?

你可能感兴趣的:(Android学习笔记,Java,JVM,java,单例模式,JVM,JMM,DLC)