大家可能在项目中都遇到过单例模式的运用,那么,单例模式究竟运用于什么情况呢?它都有哪些实现方式呢?各个方式都有什么优缺点呢?今天我就带大家来盘点一下,单例模式。
其实,单例模式在我们的项目中运用到的地方很多,其运用的目的,那么,为什么要运用单例模式呢?我们都知道,在多线程并发去执行操作的时候,保证不了数据的一致性,这是我们JMM构造所致,跟我们的CPU不同,CPU有我们的MESI(缓存一致性协议)
cpu硬件方面,我们做了一些处理,保证一致性,但是,在jmm层面,我们并没有去做任何的处理,而这只是在缓存层面上去保证的(为了减少内存和CPU之间协作的时间差异,CPU的操作时间远远高于我们的内存,防止出现CPU一直等待和内存之间的交互,就出来了L1,L2,L3(三级缓存),具体了解CPU的一级二级三级缓存),在运行时,jmm的工作区和内存是可能在寄存器和高速缓存和内存里都存在,所以说
这样交叉的形式,会导致数据不一致的问题,那么这就产生了问题:多个线程要操作同一对象(比如我之我只想new出来一个对象,只对这一个对象的属性进行操作,多线程的时候,不能保证我仅仅只创建出来一个对象),要保证对象的唯一性,这个问题怎么解决呢?
上图就是我们JMM工作时的划分和和JVM内存区域划分
上图可见,用volatile保证了我们并发操作的三大特征的可见性(其余是原子性和有序性,以后会出文章专门讲解JMM和硬件之间关系)
当运行我们的java文件的时候,其实是这样一个流程
将class字节码文件变成我们的计算机可以执行的二进制文件(010101),拆分成多个任务,进行线程的分配,然后线程转化为内核线程,这也就是我们所说的用户态转为内核态,然后通过OS的内核交给我们的CPU去执行
总而言之,多个线程要操作同一对象,没有任何措施是保证不了对象的唯一性的
就这样单例模式就发扬光大了!通过Jvm和其他的一些特性保证了多线程操作一个实例对象的时候是安全的,下面我们来一步步演示这七种单例模式
1.饿汉模式
public class HungryDemo {
private static HungryDemo hungryDemo = new HungryDemo();
public HungryDemo getInstance(){
return hungryDemo;
}
}
上图就是我们的饿汉模式的代码,为什么叫饿汉模式呢
就是所谓的迫不及待的去加载我们的对象实例,就是在类加载的时候就把我们的对象实例加载出来,我们知道,在类加载的时候,首先会把静态资源初始化完毕放在我们的jvm公共区域(方法区),而且只会被加载一次,也就是说,这个对象只会被实例化一次,所以,这样我们就能根据jvm类加载的特性保证它的唯一性了。但是,它虽然能保证线程安全的,但是性能就会有所下载,假设,我要为100个类做单例,这样的话,在类加载的时候,就会把这个一百的类全部加载到方法区,就会影响我们的程序性能,类多了,就可能会产生我们的类爆炸事件(内存溢出),而且,有的类我们并不是在项目启动的时候就必须马上用到,所以呢,没有延迟加载,这种类好长时间不使用,影响性能,懒汉模式就出现了!
2.懒汉模式
public class LazyDemo {
private static LazyDemo lazyDemo = null;
public LazyDemo getInstance(){
if (lazyDemo==null){
lazyDemo= new LazyDemo();
}
return lazyDemo;
}
}
这样的话,不就可以进行延迟加载了嘛?当使用的时候才进行初始化 ,可能有些同学还是不理解,为什么这么写就能保证延迟加载了呢?原因是:加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生
这下大家明白了吧?但是这样的话,就会有问题呢,这样的话,假设两个线程同时判断lazyDemo这个类没有进行初始化,这样的话,就会产生数据的不一致性,同时产生两个一样的实例,所以,这种懒汉模式,也不能保证我们的线程安全问题,接下来,加了synchronized关键字的懒汉模式就诞生了!
3.懒汉式+同步方法
public class LazyAndSych {
private static LazyAndSych lazyAndSych = null;
public synchronized static LazyAndSych getInstance(){
if (lazyAndSych==null){
lazyAndSych = new LazyAndSych();
}
return lazyAndSych;
}
}
通过这个关键字保证我们的方法执行的原子性,就算有并发的情况发生,也不会造成产生不同的实例的这种情况
但是呢,这还有个缺点,就是,即使这样可以进行懒加载和保证线程安全,但是,这个方法就是串行的执行了,只有一个完成了,下个线程才能执行这个方法,性能就会急剧下降,接下来呢,Dubbo-check的单例就出现了!
4.dubbo-check(双重检查机制)
public class DubboCheckLazyDemo {
private static DubboCheckLazyDemo dubboCheckLazyDemo = null;
public static DubboCheckLazyDemo getInstance(){
if (dubboCheckLazyDemo==null){
synchronized (DubboCheckLazyDemo.class){
if (dubboCheckLazyDemo==null){
dubboCheckLazyDemo = new DubboCheckLazyDemo();
}
}
}
return dubboCheckLazyDemo;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(DubboCheckLazyDemo.getInstance());
}).start();
}
}
}
通过同步代码块来防止串行化的发生,但是,为什么要加两个判断呢?不这样做的话,会有性能问题,即:发现依旧存在着性能问题,也就是说,只要DubboCheckLazyDemo方法被调用,那么就会执行同步这个操作,于是我们加个判断,当dubboCheckLazyDemo没有被实例化的时候,也就是需要去实例化的时候才去同步,这样的话就不必再去走同步方法了,这样的话,即保证了线程安全,性能又好,但是这,还有一个缺陷,我们知道,我们写的代码顺序和真正执行的代码顺序是不一样了,因为为了提高程序性能做了指令重排序,这就会有一个问题,假如我们在此对象构造器里面加了几行代码,可能由于重排序会把构造器里面执行的逻辑排在实例化对象完后,这样这些代码逻辑(比如在构造器里面new其他的对象),就不会执行,操作的时候,就会报空指针错误!
所以,这时候,我们出现了volatile+dubbo-check避免这个问题
5(重点).volatile+dubbo-check
public class volatileDubboCheck {
private static volatile volatileDubboCheck singleton = null;
public static volatileDubboCheck getInstance(){
if (singleton==null){
synchronized (volatileDubboCheck.class){
if (singleton==null){
singleton = new volatileDubboCheck();
}
}
}
return singleton;
}
}
因为volatile比较霸道,强制让其前面和后面的变量不进行重排,按顺序执行,这样就能保证不会出现NPE(NullPonitException)的问题了
6.Holder(持有者)
public class Holder {
private static class HolderTo{
private static Holder holder= new Holder();
}
public Holder getInstace(){
return HolderTo.holder;
}
}
持有者,故名思意,就是把我的对象交给其他人去进行实例化
变量交给另外一个类去持有(静态内部类)
声明类的时候,成员变量中不声明实例变量,而放到内部静态类中, 静态内部变量在类初始化的时候执行<init>方法去实例化这个变量(JVM类加载机制),而这个方法是一个同步方法,所以说,用静态内部类的这种方法去做,是可以保证线程安全的
这个是一般我们经常会用的,这个方法没有什么太大的缺陷,我也经常使用这种方式去实例化对象,嘿嘿嘿!
7.ENUM(枚举单例)
public enum EnumSingleton {
A;
public static EnumSingleton getInstance(){
return A;
}
}
先给大家举一个枚举的栗子,这样做的话,其实就可以保证它出现对象实例是线程安全的,为什么呢,因为,枚举里面声明的枚举都是常量!!记住是常量!而且A即代表EumSingleton这个对象的实例!!记住!!所以可以保证我们的线程安全,但是这样的话性能有点不太好,那好,我们来进行改造一下
public class EnumSingletonAndLazy {
private EnumSingletonAndLazy(){
}
//延迟加载
private enum EnumHolder{
INSTANCE;
private EnumSingletonAndLazy instance = null;
EnumHolder(){
instance = new EnumSingletonAndLazy();
}
private EnumSingletonAndLazy getInstance(){
return instance;
}
}
public static EnumSingletonAndLazy getInstance(){
return EnumHolder.INSTANCE.instance;
}
}
我们这样是不是完成了延迟加载,把变量交给我们的内部类去初始化,只有我们在调用内部类的方法的时候才会进行加载,这样我们的 性能是不是就会显著提升?EffectJava这本书也推荐用枚举去完成单例 上面那个代码的逻辑大家应该能看明白吧(这个枚举初始化的时候就对instance进行new了,因为声明INSTANCE这个枚举的时候已经进行了实例就会调用初始化方法,上面已经强调过了,即声明INSTANCE时完成实例化),这种方法不知道你有没有试过呢?没试过的话,赶紧尝试一下吧!
今天的介绍就到这里,大家到现在应该清楚明白了吧?不懂我们还可以交流喔,QQ:936997192
加油!