文章目录
单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。
应用场景:
- 由于配置文件是共享的资源,配置对象的读取,一般应用单例模式。
- 数据库连接池的设计一般也是采用单例模式。
- 多线程的线程池的设计一般也是采用单例模式。
1. 饿汉式
饿汉式,类加载到内存后,就实例化一个单例,JVM 保证线程安全,简单实用,推荐使用。唯一缺点:不管用到与否,类装载时都会完成实例化(其实这个也不能算是缺点,因为加载它不就是为了用它吗?不然加载它干啥)。
public class Singleton {
//构造方法私有化,防止在外部被实例化
private Singleton(){
}
//静态实例私有化,防止外部引用,直接初始化。
private static Singleton INSTANCE = new Singleton();
//提供公开的静态方法,用来在外部获取实例
public static Singleton getInstance(){
return INSTANCE;
}
}
2. 懒汉式
2.1. 单一同步锁
该方案性能太差,每次拿对象都得去获取锁,所以一般不推荐使用这种写法。
public class Singleton {
//构造方法私有化,防止在外部被实例化
private Singleton(){
}
//静态实例私有化,防止外部引用
private static Singleton INSTANCE = null;
//提供公开的静态方法,用来在外部获取实例
public static synchronized Singleton getInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
2.2. 双重校验锁
DCL
(Double Check Lock
),双重校验锁。
public class Singleton {
//构造方法私有化,防止在外部被实例化
private Singleton(){
}
//静态实例私有化,防止外部引用(volatile 禁止指令重排,保证线程修改可见性)
private static volatile Singleton INSTANCE = null;
//提供公开的静态方法,用来在外部获取实例
public static Singleton getInstance(){
if (INSTANCE == null){
//第一重校验
synchronized (Singleton.class){
//锁定
if (INSTANCE == null){
//第二重校验
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
2.2.1. volatile 的作用
(1)保证线程修改的可见性
Java 语言编写的程序,有时为了提高运行效率,编译器会自动对其优化,把经常访问的变量缓存起来,程序在读取这个变量时有可能直接从缓存(例如寄存器)中读取,而不会去内存中读取。当多线程编程时,变量的值可能因为被别的线程改变了,而该缓存的值不会相应的改变,从而造成读取的值与实际变量的值不一致。
volatile
修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
(2)禁止指令重排序
在 Java 内存模型(JMM)中,并不限制处理器的指令顺序,说白了就是在不影响结果的情况下,顺序可能会被打乱。
在执行INSTANCE = new Singleton();
这条语句时,JMM 并不是一下就执行完毕的,即不是原子性的,实质上这句命令分为三大部分:
- 为对象分配内存空间,字段赋默认值。(申请空间)
- 执行构造方法语句,初始化实例对象。 (初始化对象)
- 把
INSTANCE
引用指向分配的内存空间。(引用关联)
在 JMM 中这三个步骤中的2
和3
不一定是顺序执行的,如果线程A
执行的顺序为1
、3
、2
,在第2
步执行完毕的时候,恰好线程B
执行第一次判空语句,则会直接返回INSTANCE
,那么此时获取到的INSTANCE
仅仅只是不为null
,实质上没有初始化,这样的对象肯定是有问题的!(也就是说线程B
可能获取到半初始化状态的对象,该对象内部的各字段的值都还是各类型的默认值,并未完成值的初始化。)
而volatile
关键字的存在意义就是保证了执行命令不会被重排序,也就避免了这种异常情况的发生,所以这种获取单例的方法才是真正的安全可靠!
2.2.2. volatile 的缺点
使用volatile
屏蔽掉了 JVM 中必要的代码优化,所以在执行效率上会比较低。
2.3. 静态内部类
public class Singleton {
//构造方法私有化,防止在外部被实例化
private Singleton(){
}
//使用静态内部类维护单例
private static class SingletonFactory{
private static Singleton INSTANCE = new Singleton();
}
//提供公开的静态方法,用来在外部获取实例
public static Singleton getInstance(){
return SingletonFactory.INSTANCE;
}
}
2.4. 枚举
public class Singleton {
//构造方法私有化,防止在外部被实例化
private Singleton(){
}
//使用枚举实现单例
private enum Instance{
INSTANCE;
private Singleton singleton;
Instance(){
singleton = new Singleton();
}
public Singleton getSingleton(){
return singleton;
}
}
//提供公开的静态方法,用来在外部获取实例
public static Singleton getInstance(){
return Instance.INSTANCE.getSingleton();
}
}
2.5. CAS
以上几种实现,其实现原理都是利用了类加载的时候初始化单例,即借助了 ClassLoader 的线程安全机制。所谓 ClassLoader 的线程安全机制,就是 ClassLoader 的 loadClass 方法在加载类的时候使用了synchronized
关键字。也正是因为这样, 除非被重写,这个方法默认在整个装载过程中都是同步的,也就是保证了线程安全。
所以,以上几种方法,虽然有的实现并没有显式的使用synchronized
,但是其底层实现原理还是用到了synchronized
。
不使用锁的话,有办法实现线程安全的单例吗?有的,那就是使用CAS
。CAS
是项乐观锁技术,当多个线程尝试使用CAS
同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都会失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS
(Compare And Swap
)比较和替换是设计并发算法时用到的一种技术。
public class Singleton {
//构造方法私有化,防止在外部被实例化
private Singleton(){
}
//使用原子类包装
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
//提供公开的静态方法,用来在外部获取实例
public static Singleton getInstance(){
for (;;){
//死循环,相当于while(true){}
Singleton singleton = INSTANCE.get();
if (singleton != null){
return singleton;
}
singleton = new Singleton();
//CAS操作,如果INSTANCE为null,则把它修改为singleton
if (INSTANCE.compareAndSet(null, singleton)){
return singleton;
}
}
}
}
这种方式实现的单例有啥优缺点吗?
用CAS
的好处在于不需要使用传统的锁机制来保证线程安全,CAS
是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
CAS
的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对 CPU 造成较大的执行开销。另外,如果 N 个线程同时执行到singleton = new Singleton();
的时候,会有大量对象创建,很可能导致内存溢出。