你管这叫单例模式?
大家好,我是徒手敲代码。
今天来介绍一下单例模式。
单例模式的意思,就是对于某一个类,只能创建一个实例对象。
饿汉式和懒汉式
首先根据概念,可以写出这样的单例模式代码 demo
public class Singleton {
// 私有构造函数,严格限制入口
private Singleton() {}
// 饿汉式
private static final Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
因为目的是想只能创建一个对象,所以要把构造方法写成私有的,然后暴露一个 getInstance()
方法出来,让别的类调用这个方法来获取 Singleton
对象。
像这样还没有实际用到对象之前,就已经将对象创建出来的方式,称为饿汉式。
如果说为了尽可能节省内存的开销,可以在实际需要这个对象的时候,才创建,这种方式也称为懒汉式,代码如下:
public class Singleton {
// 私有构造函数,严格限制入口
private Singleton() {}
// 懒汉式
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面的这段小程序,在多线程环境下,肯定是没办法做到单例的,如果多个线程同时进入 if
判断,同时判断出 instance
为空,那么自然地就可以创建出来多个 Singleton
对象了。
双重检测锁
在上面懒汉式的基础上,可以在判断的时候,加个锁,只能让一个线程进入 new 对象。比如:
public class Singleton {
// 私有构造函数,严格限制入口
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
// 第一次检查
if (instance == null) {
//加锁
synchronized (Singleton.class) {
// 第二次检查
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里synchronized
锁的是这个Singleton
的类,注意不能使用对象锁哈,因为我们目的是整个类只能创建一个对象。
为什么要做第二次检查?假设 A 线程刚刚完成了对象的构建,此时有个 B 线程进来,在刚即将创建完成和创建完成的,这个临界点,B是可以获取到锁,进去 new 对象的,所以说,一定要加多一次判断。
指令重排序
其实这段代码,也不是百分百的线程安全。编译器在将我们写的代码,转换成字节码的过程中,会趁我们不注意,来一个指令重排序的操作。
比如 instance = new Singleton();
这个操作,在计算机的眼里,总共有这三个步骤:
- a. 分配对象内存空间
- b. 初始化对象成员变量
- c. 将
instance
变量指向分配的内存地址
我们会一厢情愿的认为,执行顺序就是 a → b → c
但是,实际情况是,有可能操作顺序变成 a → c → b,那么就存在一个时刻,是 instance
变量不为空,但是对象还没有创建出来;在这个时刻,如何恰巧有一个线程进来,那么就直接去到 return instance;
返回了一个处于无效状态的对象。天啊,辛辛苦苦搞过来的对象,居然是无效的!
我们在之前的文章讲过,要打破这种情况,就要加上一个volatile
,加了之后的代码如下:
public class Singleton {
// 私有构造函数,严格限制入口
private Singleton() {}
private volatile static Singleton instance = null;
public static Singleton getInstance() {
// 第一次检查
if (instance == null) {
//加锁
synchronized (Singleton.class) {
// 第二次检查
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
其他方式
其实还有其他实现单例模式的常见方法,比如用静态内部类
public class Singleton {
// 私有构造函数,严格限制入口
private Singleton() {}
private static class SingletonHolder {
// 静态内部类存放对象实例
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
// 调用时才加载静态内部类
return SingletonHolder.INSTANCE;
}
}
注意,这种方式,从外部是无法访问内部类的,只能调用getInstance()
方法来获取实例对象。
以上说的这些实现单例模式的方法,都可以用反射来破解,大致步骤是:
- 先获取
Singleton
的构造函数 - 将访问权限设置成
true
- 直接用
newInstance
方法来创建对象
代码:
//获得构造器
Constructor constructor = Singleton.class.getDeclaredConstructor();
//开启访问权限
constructor.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)constructor.newInstance();
Singleton singleton2 = (Singleton)constructor.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));
可发现结果为 false
如果要防止被反射打破单例的实现,可以用枚举的方式,代码就一行:
public enum SingletonEnum {
INSTANCE;
}
总结一下以上这几种方式的特点
实现方式 | 是否线程安全 | 是否懒加载 | 是否防止反射构建 |
---|---|---|---|
双重检测锁 | 是 | 是 | 否 |
静态内部类 | 是 | 是 | 否 |
枚举 | 是 | 否 | 是 |
今天的分享到这里结束了,如果你喜欢这种分享知识的方式,可以在下方留言喔。
——————————————————
关注我(公众号“徒手敲代码”),让知识变得简单。
回复“电子书”,免费获取大佬推荐的Java书籍
转载自:https://juejin.cn/post/7358011504966877236