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