2.设计模式-单例模式

前言

单例模式是平时接触可能最多的一种设计模式,同时相对来说也算是比较简单的设计模式。但是依然有一些细节的地方需要注意。
首先什么是单例?按字面意思那就是只允许有一个实例对象。因为对于java类而言,我们可以通过new的方式创建很多的java对象,对象是需要占用我们的内存空间,而我们在实际的业务当中并不需要这么多对象,只需要一个对象就可以满足我们的业务需求了。比如web开发中,我们的controller类,正常情况下只需要存在一个就可以了。还有一些工具类,只需要一个实例就可以处理上传文件,加载配置文件等功能。相对比较安全的写法大概有如下5种。


一、饿汉模式(推荐)

public class Demo1 {
    //直接在这里就初始化,管它需不需要,这就饿汉模式,饥渴难耐,必须先初始化
    private static  Demo1 demo1=new Demo1();
    //私有化构造方法,必须
    private Demo1(){}
    public static Demo1 getInstance(){
        return demo1;
    }
}

首先静态的成员对象会随着类的加载而初始化,而且jvm会保证只会有一个,所以这里是完全能保证只有一个对象。虽然这种方式感觉很low,还有很多人说走来就创建加载对象是不是有点不太好,占内存,没有做到按需加载。但按我的理解感觉咱也不能这么吝啬吧,这点内存都不提前给。一个大型的项目里面,实际我们自己业务封装的单例对象也算不上特别多。

二、懒汉模式

public class Demo2 {
    //静态成员变量初期不初始化对象,懒得先初始化
    private static Demo2 demo2;
    //私有化构造方法,必须
    private Demo2(){}
    public synchronized static Demo2 getInstance(){
        if(demo2==null){//1.判断对象为空则初始化,否则直接返回
            demo2=new Demo2();//2
        }
        return demo2;
    }
}

加上synchronized 关键字的目的是给方法上锁。如果不这样做,那么在多线程并发的情况下,假如第一个线程A进入到1的位置,判断demo2为空然后进入到2的位置,但还没来得及执行代码,切换到了线程B进来,然后在1的位置发现还是为空,那么也进入到2的位置。这个时候线程A和线程B就会出现创建两个对象的情况。但是这样把方法锁住了,虽然说能够完全保证对象只有一个,但实际上也造成了很多其它线程在外面等待,性能上的相对就不是很好,所以一般不按照这种写。

三、双重锁-double check

public class Demo3 {
    //静态成员变量初期不初始化对象
    private static volatile  Demo3 demo3;
    //私有化构造方法,必须
    private Demo3(){}
    public static Demo3 getInstance() {
        if (demo3 == null) {//1.判断对象为空则初始化,否则直接返回
            synchronized (Demo3.class) {//2.给对象上锁
                if(demo3==null){//3.再次判断是否为空
                    demo3 = new Demo3();//4.初始化
                }
            }
        }
        return demo3;
    }
}

这种写法在网上的呼声还是比较高,首先是按需加载,然后又相对保证了性能。因为把同步方法改成了同步代码块,同时前面有一个判断,那么类实例存在直接返回,也就不需要等待。但是写法相对没那么简单。
问题1:为什么步骤3的位置还需要进行一个空判断
同样的因为在高并发的情况下,如果不进行空判断,A线程到了步骤4的位置,但是还没有执行代码,那么线程切换到B,B进入到步骤1,发现实例为空,那么继续往下在步骤2等待。这时候切换到A线程完成实例化。那切换到B线程,如果不判断,也会进行第二次new对象,所以这里必须要判断。
问题2:为什么要加volatile关键字
这里的话就涉及到cpu和jvm执行指令的问题了,他们会对指令进行优化,就是可能进行重新排序。而demo2=new Demo2() 这个在生成指令的时候是如下3个步骤,关键在于2,3步骤可能会出现调换的情况,也就是说我对象还没有初始化完成,就已经指向了一个地址,那么这个时候另外的线程实际上得到的对象是有问题的。具体可以参考volatile关键字说明

memory = allocate();  // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory;//3.实例指向对应的地址

四、内部类(推荐)

public class Demo4 {
    private static class Demo4Holder{//定义一个内部类
        private static Demo4 demo4=new Demo4();//初始化方法
    }
    //私有化构造器,必须
    private Demo4(){}
    public Demo4 getInstance(){
        return Demo4Holder.demo4;//获取实例
    }
}

这种方式是在类被引用的时候才进行初始化操作,同时jvm能够确保只会有一个实例,写法也比较简单。所以也是一种比较好的写法。

五、 枚举(实际业务用得不多)

public enum Demo5 {
    INSTANCE; //直接申明一个变量
    public void hello(){
        System.out.println("hello world");
    }
}
public class Test {
    public static void main(String[] args) {
        //直接访问则返回一个实例
        Demo5 demo5 =  Demo5.INSTANCE;
        demo5.hello();
    }
}
输出:
hello world

枚举这种方式是effective java作者推荐的一种写法,简单明了!而且最重要的是防反射和反序列化,上面的方法默认都不能做到防反射和防反序列化,必须做一些特殊处理才行。但我们开发肯定是对类相对会熟悉一点,这种枚举方式虽然简单明了,但在实际业务当中用得并不多。

如何防反射(待补充)

如何防序列化(待补充)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值