单例模式是Java中最简单的设计模式之一,属于创建型模式,提供了一种创建对象的方式。单例模式主要解决的是,⼀个全局使⽤的类频繁的创建和消费,从⽽提升提升整体的代码的性能。
Singleton Pattern
单例设计模式是最简单的设计模式之一,其实现方式也多种多样。
1、单例的实现方式
单例的实现方式较多,主要从实现上分为是否支持懒汉式、是否线程安全等。
1.1 饿汉式-静态变量-线程安全
1 | public class Singleton01 { |
new Singleton01
会在类初始化阶段被创建:
<clinit>
会在类初始化阶段被执行,对于<clinit>()
方法的调用,也就是类的初始化,虚拟机会在内部确保线程安全性。虚拟机会确保<clinit>()
在多线程环境下能被正常的加锁、同步。如果多线程同时初始化一个类,只能有一个线程执行该类的<clinit>()
方法。
1.2 懒汉式-内部类-线程安全
1 | public class Singleton02 { |
使⽤类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的⽅式耗费性能。
这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是⼀个类的构造⽅法在多线程环境下可以被正确的加载。
此种⽅式也是⾮常推荐使⽤的⼀种单例模式。
1.3 懒汉式-双重校验锁-线程安全
1 | public class Singleton03 { |
为什么使用volatile?
instance = new Singleton03();
在底层会拆分为以下几个步骤:
a. 申请对象的内存空间
b. 初始化内存空间
c. 将instance指向分配的内存空间
由于存在指令重排序,b与c的执行顺序可以不一致,可能出现一下顺序:
a. 申请对象的内存空间
c. 将instance指向分配的内存空间
b. 初始化内存空间
这时就会出现instance不是null,而指向的地方却仍未被初始化。
假想一下情况,A线程刚好执行到c,这时刚好系统调度,B线程判断instance不是null,获取到了instance,并通过instance,而instance其实未被初始化完成,这就会出现安全问题。使用volatile修饰可以避免该变量在操作时发生指令重排序,可以避免以上隐患的发生。
1.4 懒汉式-CAS-线程安全
1 | public class Singleton04 { |
java并发库提供了很多原⼦类来⽀持并发访问的数据安全 性; AtomicInteger 、 AtomicBoolean 、 AtomicLong 、 AtomicReference 。
AtomicReference 可以封装引⽤⼀个V实例,⽀持并发访问如上的单例⽅式就是使⽤了这样的⼀个特点。
使⽤CAS的好处就是不需要使⽤传统的加锁⽅式保证线程安全,⽽是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。
相对于其他锁的实现没有线程的切换和阻塞也就没有了额外 的开销,并且可以⽀持较⼤的并发性。
当然CAS也有⼀个缺点就是忙等,如果⼀直没有获取到将会处于死循环中。
1.5 饿汉式-枚举-线程安全
1 | public enum Singleton05 { |
Effective Java作者推荐,枚举类型是线程安全的,由JVM保证,枚举的写法非常简单,而且枚举类型是 所用单例实现中唯一一种不会被破坏的单例实现模式。
2、单例的使用场景
单例的使用场景随处可见,JDK中Runtime类就实现了单例设计模式。
除此之外,单例还被利用在Spring中⼀个单例模式bean的⽣成和使⽤。
3、总结
单例模式的实现可以分为线程是否安全、是否懒加载。
- 懒加载
- 双重检查锁:synchronized代码块代替synchronized方法,省去不必要的锁,大大提高性能
- 内部类:既实现了懒加载,又不用加锁
- CAS:利用了CAS的特性,不加锁,但是循环太久会浪费资源
- 饿汉式
- 静态变量实现:利用类【初始化】会执行
<clinit>
实现实例的创建 - 枚举:JVM提供线程安全支持,Effective Java作者推荐
- 静态变量实现:利用类【初始化】会执行
之所以区分懒加载,一部分原因就是因为一些使用周期比较短的类,如果过早的被创建而却不被使用,会存在内存的浪费(有些人也称为广义上的内存泄漏),因此对于懒饿的使用应该根据使用场景而定,比如JDK的Runtime由于一启动就要使用,因此使用饿汉式来实现。