目录
🔥饿汉模式
💧结果
🔥懒汉模式
⚡加锁
⚡ 双重if
⚡ 防止指令重排序
🔥总结
单例模式是一种设计模式,设计思路,我们这里主要学习了其中的饿汉模式和懒汉模式。其中,饿汉模式天生是线程安全的,懒汉模式则需要我们通过学过的方法保证其的线程安全。
饿汉模式
什么是饿汉模式,就如字面意思,表示干什么都很着急,不管用不用都需要提前准备好。
例如,读取硬盘中文件的内容并显示出来
饿汉模式:将文件中的内容都读到内存中并一股脑全显示出来。如果文件内容比较大,这样势必 会让内存吃紧,导致卡顿。
public class Singleton {
private static Singleton singleton = new Singleton();
public static Singleton getSingleton(){
return singleton;
}
//手动将构造方法权限改为private,外部无法再new
private Singleton(){
}
//随便写个方法,方便观察
public int test(int n){
return n*2;
}
}
使用static修饰,表示类属性这样保证问我们创建出来的是唯一实例对象。
通过get方法来获取引用。
将构造方法设为private,外部无法通过构造方法创建其他实例对象。也保证了单例模式的唯一实例
public class Main {
public static void main(String[] args) {
Singleton instance = Singleton.getSingleton();
System.out.println(instance.test(100));
}
}
结果
饿汉模式本身是线程安全的, 我们无需使用加锁之类的方法保证线程安全。
懒汉模式
懒汉模式就与饿汉模式相反,饿汉模式是提前将需要的都加载上,而懒汉模式就是非必要不创建,只有在需要使用实例对象时才会创建。
例如,从硬件中读取文件中的内容并显示出来
懒汉模式:读取文件中的一部分并显示出来,当想要看后面的内容时,在从硬盘中读再显示。
public class LazySingleton {
private static LazySingleton lazySingleton = null;
public static LazySingleton getLazySingleton(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
private LazySingleton(){
}
}
跟饿汉模式相比,我们在加载类的时候并没有就创建出实例,而是先设置为null,当调用到了get方法,也就是说需要用到了实例对象,这时候我们再创建实例对象。
线程不安全
加锁
在多线程环境下,由于线程的抢占式执行,例如t1线程在执行完目前代码的if之后还没有进行实例化对象就被调走,而t2线程来了,完成了new操作,此时t1线程回到cpu上继续执行也进行了new操作,就会产生两个不同的实例对象。
所以,为了解决上面问题,我们就需要保证判断和new操作的原子性,也就是针对这段逻辑进行加锁(synchronized)。
双重if
我们仔细想后便知道,出现创建两个不同的实例对象的情况只会在第一次创建对象的时候。加锁是一个低效的操作,但是后续使用中并不会出现创建两个对象的情况,所以我们需要使用双重if来提高效率(非必要,不加锁)。
此时内外两个if判断的条件虽然一样,但是两个if的作用,解决的问题不同,两个if缺一不可,各自有各自的作用。第一个if防止重复加锁,第二个if保证是否要创建实例对象。
防止指令重排序
我们知道 new 操作并不是一步操作,在执行的时候会分为三步执行,先申请内存空间,再通过构造方法创建实例对象,最后将内存地址赋值给引用。
针对这个问题我们可以使用volatile来解决。
总结
饿汉模式天生的是线程安全的。不需要使用加锁,volatile等一些操作。 但是饿汉模式的缺点也很明显,就是创建实例对象的时机太早了,任何操作的作用都是一次性要完成,可能导致内存不够,卡顿等问题。
懒汉模式并不是线程安全的,通过上面分析,我们需要使用三步来保证线程安全。
1.加锁,保证第一次创建实例对象的操作原子性。
2.双重if,非必要不加锁,保证重复的加锁操作。
3.volatile防止指令重排序(new操作),保证后续能拿到完整的对象。