单例模式
单例模式
解释:一个类只能有一个实例
单例模式可以分为两种 饿汉式 和 懒汉式
饿汉式
也被称为预加载,即 在加载类的时候,就将实例创建出来,加载到内存,不管之后会不会使用这个实例
主打一个饥不择食,体现了贪心的思想。
java
public class HungrySingleton {
//方式1:静态变量
private static HungrySingleton instance = new HungrySingleton();
//方式2:静态代码块
/*static {
instance = new HungrySingleton();
}*/
//方式3:枚举,可以看 EnumSingleton.class
public static HungrySingleton getInstance(){
return instance;
}
}
很明显,我们还没有使用该对象,就已经加载到了内存,浪费内存
但是,同时,因为只有一此创建对象,所以饿汉式是线程安全的
懒汉式
也成为懒加载 ,即:只有在使用该类时才创建需要的对象 。
我很懒,你不用我,我就不创建对象
一般有四种实现方式
- 简单懒汉式,不能用,线程不安全
- Synchronized 同步方法,一般不用,线程安全,但锁粒度太大,效率较低
- 双重检查锁,可以用 ,注意使用volatile 关键字保证单例对象的原子性
- 静态内部类,推荐使用
java
public class LazySingleton {
private volatile static LazySingleton instance;
/**
* 简单懒汉式
* 问题:线程不安全,一般不用
* @return
*/
public static LazySingleton getInstance01() {
if(null == instance){
instance = new LazySingleton();
}
return instance;
}
/**
* synchronized
* 解决了线程安全问题,但效率低,一般不用
* @return
*/
public synchronized static LazySingleton getInstance02() {
if(null == instance){
instance = new LazySingleton();
}
return instance;
}
/**
* 双重检查锁模式
* 降低锁的粒度,只锁创建对象的代码块
* 需要增加 volatile 来保证原子性,防止jvm指令重排,但同时屏蔽了JVM的一些代码优化
* @return
*/
public static LazySingleton getInstance03() {
if(null == instance){
synchronized (LazySingleton.class){
if(null == instance){//防止重复创建对象
instance = new LazySingleton();
}
}
}
return instance;
}
/**
* 静态内部类方式
* 只有在使用时才会创建静态内部类,推荐使用
* @return
*/
public static LazySingleton getInstance04() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder{
private static final LazySingleton INSTANCE = new LazySingleton();
}
}
这里解释下 为什么 双重检查锁要 使用 volatile 关键字 修饰单例对象
if判断以及其内存执行代码是非原子性的。其次,new LazySingleton()无法保证执行的顺序性。
显然,不满足原子性或者顺序性,线程肯定是不安全的。
下面主要讲一下 new LazySingleton() 为什么不能保证顺序性。
设想一下,创建一个对象,应该分为几步?
答案是三步,如下:
memory=allocate();//1:初始化内存空间
ctorInstance(memory);//2:初始化对象
instance=memory();//3:设置instance指向刚分配的内存地址
jvm为了提高程序执行性能,会对没有依赖关系的代码进行重排序 ,也就是说上面2和3行代码可能被重新排序。
我用两个线程举例:
时间片 | 线程A | 线程B |
---|---|---|
t1 | 初始化内存空间 | |
t2 | 设置instance指向刚分配的内存地址 | |
t3 | 判断instance 是否为空 | |
t4 | 由于instanc不为空,获取到一个空的实例化对象(线程不安全) | |
t5 | 初始化对象 |
volatile 关键字就是强制要求jvm 不进行指令重排,按顺序执行,当然也有一定的性能损失
volatile 其他相关知识 可以参考以下文章:
https://blog.csdn.net/m0_50370837/article/details/124380385