一、综述
单例模式就是一个类只能有一个实例对象。
单例模式分为两种:1,懒汉式单例 2,饿汉式单例 他们又分别有传统实现和优化的推荐实现
二、懒汉式单例:当需要使用对象的时候才进行实例化
由于可能有多个线程同时要使用对象,因此需要考虑线程安全问题,防止并发访问时生成多个实例。通常需要加锁来解决并发冲突,是用时间换空间的方案。
1.传统的写法
java
class Singleton{
//设置私有构造方法
private Singleton(){}
//声明一个Singleton对象为obj
private static Singleton obj;
//加锁保证obj只能实例化一次,时间换空间
public static synchronized Singleton getInstance(){
if(obj == null){
obj=new Singleton();
}
return obj;
}
}
传统实现方式中,每次获取实例都要被synchronized关键字串行化,即使已经生成了对象实例。
而我们加锁的目的是为了防止生成多个实例,因此其实只需要对生成实例的代码加锁,生成实例后,可支持并发访问,提高性能。
2.优化实现代码:双检法
java
class Singleton{
//设置私有构造方法
private Singleton(){}
// 最后解释volatile关键字
private volatile static Singleton obj;
//获取实例对象的方法
public static Singleton getInstance(){
//如果已有实例则直接返回,不走锁,避免无意义加锁
if(obj==null){
//仅在没生成实例时加锁控制,使并发访问串行化
synchronized(Singleton.class){
//多个线程会按序执行到此处,需要再次检查是否已经实例化
if(obj==null){
obj = new Singleton();
}
}
}
return obj;
}
}
线程 A:通过外层if → 拿到锁 → 执行内层if(obj 为 null)→ 创建实例 → 释放锁。
线程 B:之前在锁外等待 → 线程 A 释放锁后,线程 B 拿到锁 → 执行内层if(此时 obj 已经被线程 A 创建,不为 null)→ 直接退出同步块,不会重复创建实例。
2.1为什么使用volatile关键字
因为使用new来创建对象不是一个原子操作(不可分割的操作序列,要么都成功,要么都失败),而是会被编译成如下三条指令:
- 给实例分配内存
- 初始化实例的构造
- 将实际对象指向分配的内存空间(此时实例应该已经不为空)
正常的思路是123一定按顺序执行。
但事实上,Java会对进行指令重排序。
即JVM虚拟机在执行上面三条指令时,可能按照132的顺序执行。
假设当13执行完,2还未执行时,如果另外一个线程调用getInstance(),会在判断对象是否为null时返回false(因为3已执行,对象指向了内存空间,已不为空),然后直接返回实例。但由于此时2还没执行,实例并未完全初始化,只是分配了内存空间,就会导致使用对象时出现错误(引用逃逸)。
而voliate关键字可以通过内存屏障禁止指令重排序,保证创建对象时的123步骤按顺序执行,从而解决上述问题。
三、饿汉式单例
1.传统实现
通过static关键字,在类加载时创建对象。
java
Class Singleton{
//私有构造方法
private Singleton(){ };
//类加载时就实例化对象 加static
private static Singleton obj=new Singleton();
public static Singleton getInstance(){
return obj;
}
}
2.优化实现
由于类加载时就实例化对象,因此当我们调用这个类的其它静态方法时,可能并不需要实例对象,也会触发类加载,从而实例化单例独享,会导致空间的暂时浪费。
由于静态内部类中的对象不会默认加载 ,直到调用了获取该内部类属性的方法。因此可用静态内部类封装静态实例变量。
java
class Singleton{
// 私有构造函数
private Singleton() {}
// 静态内部类
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}