2.设计模式-单例模式

tech2022-08-06  134

文章目录

前言一、饿汉模式(推荐)二、懒汉模式三、双重锁-double check四、内部类(推荐)五、 枚举(实际业务用得不多)如何防反射(待补充)如何防序列化(待补充)

前言

单例模式是平时接触可能最多的一种设计模式,同时相对来说也算是比较简单的设计模式。但是依然有一些细节的地方需要注意。 首先什么是单例?按字面意思那就是只允许有一个实例对象。因为对于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作者推荐的一种写法,简单明了!而且最重要的是防反射和反序列化,上面的方法默认都不能做到防反射和防反序列化,必须做一些特殊处理才行。但我们开发肯定是对类相对会熟悉一点,这种枚举方式虽然简单明了,但在实际业务当中用得并不多。

如何防反射(待补充)

如何防序列化(待补充)

最新回复(0)