单例模式所有设计模式中的 创建型模式。
保证一个类从始到终仅有一个实例,同时这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要手动实例化该类的对象。
单例模式的核心:
1. 该类只能有一个实例
2. 该类必须自己创建自己的唯一实例。
3. 该类必须给所有其他对象提供这一实例。
4. 该类的构造函数必须是私有的。
优点
缺点
使用场景
常见使用场景:计数器、数据库连接池、线程池、Spring相关Bean 比如 Service、Controller 默认就是单例的。
顾名思义,在初始化时 就进行对象的实例化
public class Instance {
// 私有静态 的 对象实例
private static Instance instance = new Insance();
// 私有化构造方法
private Instance () {
}
// 获取 实例的 方法
public static Instance getInstance() {
return instance;
}
}
在第一次用到的时候才进行实例化
public class Instance {
private static Instance instance;
// 私有化构造方法
private Instance () {
}
// 获取 实例的 方法
public static Instance getInstance() {
if(instance == null) {
instance = new Instance();
}
return instance;
}
}
懒汉式在多线程下的线程安全问题:
假设有两个线程A ,B,同时调用getInstance()方法。
1. 线程A执行到 if(instance == null) { 这行代码,条件成立
2. 线程A进入执行instance = new Instance();
3. 这时 线程B 执行到 if(instance ==null) { 这行代码,
4. 此时线程A 执行instance = new Instance();还没有结束
5. 所以线程B 判断 if(instance ==null)条件也成立
6. 那么线程B也 进入执行instance = new Instance();
7. 这就会 导致两个线程返回的不是同一个对象。造成了线程安全问题。
解决方式:
但是仔细思考一下,如果直接加锁的话,每次都只能一个线程执行的话,岂不是会导致效率极低?有没有 既可以保证线程安全 又在一定程度上提高效率 的方式,看下面:
public class Instance {
private static Instance instance;
// 私有化构造方法
private Instance () {
}
// 获取 实例的 方法
public static Instance getInstance() {
if(instance == null) {
// 先判断一次,在进行加锁,比直接加锁,效率要更高。
synchronized(Instance.class){
if(instance == null) {
instance = new Instance();
}
}
}
return instance;
}
}
到这里,懒汉式的单例模式就比较完美了。但是还是有一个小的缺陷。这里就要说一下指令重排的问题了。
双端检索机制存在的指令重排问题
首先来了解一下对象的实例化过程:
Instance instance = new Instance(); 这句代码一共包含几步操作?
1. memory = allocate(); 分配对象内存空间
2. instance (memory); 初始化对象
3. instance = memory; 设置 instance 指向刚分配的内存地址,此时 instance != null
这是正常情况下对象的实例化过程。分配空间 =》初始化对象 =》引用指向内存地址
指令重排
但是,JVM可能在底层做自动优化。叫做指令重排。在指令重排的情况下,上述步骤 就可能会变为 1 =》3 =》2。也就是 分配空间 =》引用指向内存地址 =》初始化对象。 那么在执行步骤2(引用指向内存地址)时,instance != null,但是实际上对象还没有初始化完成。
场景:假设两个线程A,B
1. 线程A执行到 if(instance == null) { 这行代码,条件成立
2. 线程A进入执行instance = new Instance(); 这时JVM做了指令重排序。
3. 这时 线程B 执行到 if(instance ==null) { 这行代码,不成立。但是线程A new对象的过程还没完成
4. 线程B直接返回instance,但是最终返回的还是一个空的对象,这就造成了线程安全问题。
解决方案就是使用 volatile 禁止JVM的指令重排。
public class Instance {
private volatile static Instance instance;
// 私有化构造方法
private Instance () {
}
// 获取 实例的 方法
public static Instance getInstance() {
if(instance == null) {
// 先判断一次,在进行加锁,比直接加锁,效率要更高。
synchronized(Instance.class){
if(instance == null) {
instance = new Instance();
}
}
}
return instance;
}
}
public class Instance {
// 私有化构造方法
private Instance () {
}
// 获取 实例的 方法
public staic final Instance getInstance() {
return InstanceInner.INSTANCE;
}
// 私有静态内部类
private static class InstanceInner {
private static final Instance INSTANCE = new Instance();
}
}
需要注意的是,以上的方式,都可以通过反序列化或反射的方式来进行实例化对象。所以某种长度上,缺乏安全性。
public enum Instance {
INSTANCE;
public Instance getInstance(){
return INSTANCE;
}
}
枚举方式实现单例模式的优点:
1. 能够防止反序列化重新创建对象。
2. 能够避免通过反射方式攻击单例模式。
3. 能够避免线程安全问题。