Java性能 -- 单例模式
饿汉模式
class
1 | // 饿汉模式 |
- 使用了
static
修饰了成员变量instance,所以该变量会在类初始化的过程中被收集进_类构造器<clinit>
_ - 在多线程场景下,JVM会保证只有一个线程能够执行该类的
<clinit>
方法,其它线程将会被阻塞等待- 等到唯一的一次
<clinit>
方法执行完成后,其它线程将不会再执行<clinit>
方法,转而执行自己的代码 - 因此,static修饰的成员变量instance,在多线程的情况下能保证只实例化一次
- 等到唯一的一次
- 在类初始化阶段就已经在堆内存中开辟了一块内存,用于存放实例化对象,所以也称为饿汉模式
- 可以保证多线程情况下实例的唯一性,而且getInstance直接返回唯一实例,性能很高
- 在类成员变量比较多,或者变量比较大的情况下,这种模式可能会在没有使用类对象的情况下,一直占用堆内存
- 第三方框架一般不会采用饿汉模式来实现单例模式
enum
1 | // 饿汉模式 + 枚举实现 |
1 | // javap -v |
- Java中的
enum
是一种语法糖!!,在javac编译后,枚举类中的枚举域会被声明为static属性 - 并且枚举域的初始化会被收录进静态代码块
static{}
,类初始化时会被收录进<clinit>
,由JVM保证线程安全
懒汉模式
Double-Check
1 | // 懒汉模式 |
- 懒汉模式是为了避免加载类对象时提前创建对象的一种单例模式
- 懒汉模式使用了懒加载方式,只有当系统使用到类对象时,才会将实例加载到堆内存中
- 上面的代码在多线程环境下,可能会出现实例化多个类对象的情况
- 当线程A进入到if判断条件后,开始实例化对象,此时instance依然为null
- 又有线程B进入到if判断条件,之后也会通过判断条件,进入到方法又创建一个实例
- 可以对方法进行加锁,保证多线程下仅创建一个实例
1 | // 懒汉模式 + synchronized同步锁 |
- 同步锁会增加锁竞争,带来系统性能开销,从而导致系统性能下降,因此这种方式也会降低单例模式的性能
- 可以考虑将同步锁放在if条件里面,减少锁粒度,进而减少同步锁的资源竞争
1 | // 懒汉模式 + synchronized同步锁 |
- 上述代码依然会创建多个实例,因为在多线程并发情况下,可以同时通过if判断条件
- 因此需要在同步锁里面再加一个判断条件,即
Double-Check
1 | // 懒汉模式 + synchronized同步锁 + Double-Check |
- 在JVM中,重排序是非常重要的一环,尤其在并发编程中,JVM可能会进行任意排序以提高程序性能
- 上面代码
instance = new Singleton()
的执行过程- 给Singleton分配内存
- 调用Singleton的构造函数来初始化成员变量(即list)
- 将Singleton对象指向分配的内存空间(执行完之后instance为非null)
- 如果JVM发生重排序优化,上面的步骤3可能会发生在步骤2之前
- 如果初始化线程A刚好完成步骤3,而步骤2没有进行时,又有另一个线程B到了第一次判断
- 线程B判断instance为非null,直接返回对象(未完成构造)使用,可能会导致异常
- synchronized只能保证可见性、原子性,但无法保证同步块内执行的顺序!!
- volatile可以保证线程间变量的可见性,同时还可以阻止局部重排序的发生,代码如下
1 | // 懒汉模式 + synchronized同步锁 + Double-Check |
内部类
1 | // 懒汉模式 + 内部类实现 |
enum
1 | // 懒汉模式 + 内部类 + 枚举实现 |
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.