一、定义
单例模式是一种创建型设计模式,有以下特点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
二、实现方式
(一)饿汉式
饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。
这种方式比较常用,但容易产生垃圾对象。
优点: 没有加锁,执行效率会提高。
缺点: 类加载时就初始化,浪费内存。它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果
//饿汉式单例类.在类初始化时,已经自行实例化
public class Singleton {
//私有化构造函数
private Singleton() {}
private static final Singleton1 single = new Singleton();
//静态工厂方法
public static Singleton1 getInstance() {
return single;
}
}
(二)懒汉式
顾名思义就是实例顾名思义就是实例在用到的时候才去创建
public class Singleton {
private static Singleton instance;
// 构造器私有,其他类就无法通过new Singleton() 来创建对象实例了
private Singleton() { }
// 获取实例的方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。(可以通过反射机制获取,暂时忽略这个问题)
但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全
1. 加同步锁
public class Singleton {
private static Singleton instance;
private Singleton() {
}
//加入synchronized
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
2.双重检查锁定
public class Singleton {
/*
volatile 修饰,
singleton = new Singleton() 可以拆解为3步:
1、分配对象内存(给singleton分配内存)
2、调用构造器方法,执行初始化(调用 Singleton 的构造函数来初始化成员变量)。
3、将对象引用赋值给变量(执行完这步 singleton 就为非 null 了)。
若发生重排序,假设 A 线程执行了 1 和 3 ,还没有执行 2,B 线程来到判断 NULL,B 线程就会直接返回还没初始化的 instance 了。
volatile 可以避免重排序。
*/
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
//减少性能开销。
if (singleton == null) {
synchronized (Singleton.class) {
//避免生成多个对象实例
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。
3.静态内部类
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
静态内部类的方式效果类似双检锁,但实现更简单。但这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这个实现思路中最主要的一点就是利用类中静态变量的唯一性。
(三)枚举
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。
它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
System.out.println("哈哈");
}
}
这种方式的原理是什么呢?趁这个机会在这里好好梳理一下枚举的概念。
枚举是 JDK5 中提供的一种语法糖,所谓语法糖就是在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是但是更方便程序员使用。只是在编译器上做了手脚,却没有提供对应的指令集来处理它。
其实 Enum 就是一个普通的类,它继承自 java.lang.Enum 类,这个可以通过反编译枚举类的字节码来理解。
使用 javac Singleton.java 得到字节码文件 Singleton.class 使用 javap Singleton.class 反解析字节码文件可以得到下面的内容:
public final class Singleton extends java.lang.Enum<Singleton> {
public static final Singleton INSTANCE;
public static Singleton[] values();
public static Singleton valueOf(java.lang.String);
public void whateverMethod();
static {};
}
枚举其实底层是依赖Enum类实现的,这个类的成员变量都是 static 类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以他天然是线程安全的。
javap 是 jdk 自带的反解析工具。它的作用就是根据 class 字节码文件,反解析出当前类对应的 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
由反编译后的代码可知,INSTANCE 被声明为 static 的,虚拟机会保证一个类的 <clinit>()方法在多线程环境中被正确的加锁、同步。所以,枚举实现在实例化时是线程安全。
另外 Java 规范中规定,每一个枚举类型及其定义的枚举变量在 JVM 中都是唯一的,因此在枚举类型的序列化和反序列化上,Java 做了特殊的规定。在序列化的时候 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象,因此反序列化后的实例也会和之前被序列化的对象实例相同。
三、问题
问题1: 为什么构造函数要使用 private
构造器私有,其他类就无法通过 new Singleton() 来创建对象实例
问题2:双重校验锁—为什么使用 volatile 和两次判空校验
-
为什么要进行两次非空校验?
- 第一个 if 判断是为了减少性能开销。
- 第二个 if 判断是为了避免生成多个对象实例。
-
为什么要用 volatile 关键字?
- 为了禁止 JVM 的指令重排,指令重排会导致对象未初始化的情况,造成报错。
问题3:单例模式中唯一实例为什么要用静态?
因为 getInstance() 是静态方法,而静态方法不能访问非静态成员变量,所以 instance 必须是静态成员变量
为什么 getInstance() 是静态方法?
因为构造器是私有的,程序调用类中方法只有两种方式, ① 创建类的一个对象,用该对象去调用类中方法; ② 使用类名直接调用类中方法,格式“类名.方法名()”;
Singleton instance = Singleton.getInstance();
构造函数私有化后第一种情况就不能用,只能使用第二种方法。
为什么要私有化构造器呢?
目的是禁止其他程序创建该类的对象。如果构造函数不是私有的,每个人都可以通过 new Singleton() 创建类的实例,因此不再是单例。根据定义,对于一个单例,只能存在一个实例。
问题4:单例模式中成员变量为什么一定要是私有的private
运行结果为null;上面可以看做是一个单例模式,下面是调用该类并将单例的成员变量改成null。
万一有程序员这么做了,后面的程序员再用这个类时就是空,所以为了安全不要这么写
问题5:为什么静态内部类写法中,静态类里面获取单例对象要用 final 修饰
用 final 更多的意义在于提供语法约束。毕竟你是单例,就只有这一个实例,不可能再指向另一个实例。instance有了 final 的约束,后面再有人不小心编写了修改其指向的代码就会报语法错误。
这就好比 @Override 注解,你能保证写对方法名和参数,那不写注解也没问题,但是有了注解的约束,编译器就会帮你检查,还能防止别人乱改—— 公众号《Java课代表》作者
问题6:单例饿汉式为什么没有线程安全性问题?
在 getInstance() 获取实例的方法中,没有对资源进行非原子性操作,instance 在类加载过程中就实例化了
我们知道,出现线程的安全性问题要满足下面三个条件:
- 多线程环境下
- 多个线程共享一个资源
- 对资源进行非原子性操作
而对于单例饿汉式不满足第三个条件
类加载过程的线程安全性保证
饿汉、静态内部类、枚举均是通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。
这其实是利用了ClassLoader 的线程安全机制。ClassLoader的loadClass 方法在加载类的时候使用了synchronized 关键字。
所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。
枚举其实底层是依赖 Enum 类实现的,这个类的成员变量都是 static 类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以他天然是线程安全的,所以,枚举其实也是借助了synchronized的
问题7:怎么不使用 synchronized 和 lock 实现一个线程安全的单例吗?
以上实现主要用到了两点来保证单例,一是JVM的类加载机制,另一个就是加锁了。那么有没有不加锁的线程安全的单例实现吗?
答:CAS实现单例
什么是 CAS?
CAS 是一项乐观锁技术,当多个线程尝试使用 CAS 同时更新一个变量时,只有其中一个线程能更新成功,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
代码实现
CAS 实现单例:
public class Singleton {
// AtomicReference 提供了可以原子的读写对象引用的一种机制
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
// 私有化构造器
private Singleton() {
}
// 获取实例的 getInstance() 方法
public static Singleton getInstance() {
for(;;) {
// 从 INSTANCE中 获取实例
Singleton singleton = INSTANCE.get();
// 如果实例不为空就返回
if (null != singleton) {
return singleton;
}
// 实例为空就创建实例
singleton = new Singleton();
// compareAndSet() 主要的作用是通过比对两个对象,然后更新为新的对象
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
使用 CAS 实现的单例有没有什么优缺点呀?
优点:
用 CAS 的好处在于不需要使用传统的锁机制来保证线程安全,CAS 是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
缺点:
CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对 CPU 造成较大的执行开销。
另外,代码中,如果 N 个线程同时执行到 singleton = new Singleton(); 的时候,会有大量对象被创建,可能导致内存溢出。
问题8:静态内部类与双重校验锁的区别?
静态内部类使用静态关键字去保证我们实例是单例的。
而我们的双重校验锁采用 lock 锁保证安全的。
问题9:为什么静态内部类写法中,静态类里面获取单例对象要用 final 修饰
用 final 更多的意义在于提供语法约束。毕竟你是单例,就只有这一个实例,不可能再指向另一个实例。instance有了 final 的约束,后面再有人不小心编写了修改其指向的代码就会报语法错误。
这就好比 @Override 注解,你能保证写对方法名和参数,那不写注解也没问题,但是有了注解的约束,编译器就会帮你检查,还能防止别人乱改—— 公众号《Java课代表》作者
四、破坏单例模式的方式
(二)防止反射破坏
public class Singleton {
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 私有的构造方法
private Singleton() {
// 防止反射创建多个对象
if(SingletonHolder.INSTANCE != null){
throw new RuntimeException("不允许创建多个实例");
}
}
// 公有的获取实例方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
(四)防止序列化破坏单例模式
public class Singleton implements Serializable {
private static final long serialVersionUID = -4264591697494981165L;
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 私有的构造方法
private Singleton() {
// 防止反射创建多个对象
if(SingletonHolder.INSTANCE != null){
throw new RuntimeException("不允许创建多个实例");
}
}
// 公有的获取实例方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
// 防止序列化创建多个对象,这个方法是关键
private Object readResolve(){
return SingletonHolder.INSTANCE;
}
}
应用
java.lang.Runtime就是经典的单例模式(饿汉式)
总结
我们来总结下
- 一般情况下,懒汉式(包含线程安全和线程不安全梁总方式)都比较少用;
- 饿汉式和双检锁都可以使用,可根据具体情况自主选择;
- 在要明确实现 lazy loading 效果时,可以考虑静态内部类的实现方式;
- 若涉及到反序列化创建对象时,大家也可以尝试使用枚举方式。