设计模式之单例模式

当你写作业的时候,你肯定是想有个分身来帮你做作业,然后你就舒舒服服吃着薯片、喝着肥宅水看电视。但是等到你领工资的时候,你肯定不想自己分成几个人然后把你工资领了~(ˉ▽ˉ;)…或者,怎么确保你就是你,不是你的克隆人…


前言

世界上没有两片完全一样的雪花

抛开程序不谈,先好好想想一下这个世界。世界上没有两片完全一样的雪花世上没有感同身受,只有冷暖自知。是不是很神奇?o( ̄▽ ̄)ブ先辈的经历都告诉我们,这个世界无论是在物质上还是在精神上,每个物体或者个体都是独一无二的┑( ̄Д  ̄)┍。这种思想映射到程序就是一种设计思想,一种设计模式——单例模式


一、什么是单例模式?

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
Java对单例模式的定义为:一个类有且仅有一个实例,并且自行实例化向整个系统提供。
用人话来说就是,当你的女神要交课程大作业的时候,你先看看自己有没有准备,如果已经准备了答案的话就直接给你的女神(饿汉模式),如果没有准备的话你就先写完然后再给你的女神(懒汉模式)。女神只要找到你就能拿到课程大作业答案~(什么是舔狗的自我修养இ௰இ)


二、什么是饿汉模式

作为一个卷王,即使我是一只卑微的舔狗,我也要成为舔狗中的卷王!等到我的女神们发现离不开我,等到其他舔狗都被我卷死,我就能成功上位(想桃子吃~)

1.概念

饿汉模式就是在类加载的时候迫不及待,立刻实例化。(相当于发布了课堂大作业就立刻写答案),后续使用就会直接调用这一份实例(就是说你的女神们问你都只会拿到这一份作业)。

2.代码展示

public class HungrySingle {
	
//在类加载的时候就实例化了,类加载只有一次,所以值实例化出了一份该实例对象
    private static HungrySingle instance = new HungrySingle();
    public static HungrySingle getInstance() {
        return instance;
}

3.特点

  • 私有类的构造器
  • 提供该类的静态修饰的对象
  • 提供公开的静态方法,返回该类的唯一对象

4.优缺点

  • 优点:对象提前创建好了,使用的时候无需等待,效率高
  • 缺点:对象提前创建,所以会占据一定的内存,内存占用大
    以空间换时间

三、什么是懒汉模式

一昧的做舔狗是没出息的,纯纯工具人。我才不在一颗树上吊死,做那么多无用功简直浪费时间,大佬都是最后才登场的。女神以为我是她鱼塘里面的一条鱼,但是有没有一种可能,我也有很多女神,谁是谁鱼塘里面的一条鱼还说不定呢~(●’◡’●)

1.概念

懒汉模式就是说用的时候才会进行实例化,但是这种临时抱佛脚的做法会出现很多奇怪的情况~

2.代码展示

如果只有一个女神找你的话,代码应该是这样子的~
代码如下(示例):

public class LazySingle {
    private static LazySingle instance = null;
    //只有在调用该方法的时候才实例化
    public static LazySingle getInstance() {
        if(instance == null) {
            instance = new LazySingle();
        }
        return instance;
    }
}

问题来了,同时两个女神都来找你,你已经写的焦头烂额咯(ˉ▽ˉ;)…居然没发现是一样的,然后你又准备写一份作业/(ㄒoㄒ)/~~

简单来说,多线程环境下,多个线程同时到达这个方法,都认为没有实例化这个对象,然后就同时实例这个对象。直接导致生成两个相同的对象,违反了单例模式的规定。那该怎么做呢?说实话,可以狠心一点,一次只和一个女神聊天~在程序上就是加锁咯

public class LazySingle {
    private static LazySingle instance = null;
    //只有在调用该方法的时候才实例化
    public static synchronized LazySingle getInstance() {
        if(instance == null) {
            instance = new LazySingle();
        }
        return instance;
    }
}

其实这种方法已经很棒了,但是这样子抛弃其他女神,说实话真不行=,=。只能想其他方法咯~

加锁的方法虽然保证了线程安全,但是每次调用该方法都得加锁,加锁就会导致程序只能串行执行!加锁后就无法进行高并发的操作,为了提高程序的执行效率,还需要继续优化。


其实不用那么死板的嘛,虽然每次都只和一个女神聊天,但是每个女神找我,我都先看看这个作业做了没有=,=,做完就能直接给女神,这样子就不用重复做一样的作业对吧🤭(时间管理大师~)

双重if判断加锁提高效率,在Java中需要配合volatile来实现

public class SecurityLazyModle {
    private LazySingle() {
    }
    private static volatile SecurityLazyModle instance = null;//保证内存可见性,防止编译器过度优化(指令重排序)
    public static SecurityLazyModle getInstance() {
        if(instance == null) {
            synchronized (SecurityLazyModle.class) {
                if(instance == null) {
                    instance = new SecurityLazyModle();
                }
            }
        }
        return instance;
    }
}

代码分析:第一层if是为了判断当前是否已经把实例创建出来,第二层synchronized是为了使进入当前if中的线程来竞争锁,当拿到锁的线程进入到第三层if之后判断是否为空,不为空就是实例化对象,然后再释放锁,释放锁之后,instance已经不为空了,后面的线程就被阻挡在了第三层if这里了,之后再来访问getInstance()方法,发现该instance已经不为空了,也就不用再抢占锁资源了,因为竞争锁也消耗大量的时间。通过这样处理,既保证了线程安全,也提高了效率。
知识补充:
指令重排序:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

这里使用volatile是为了防止编译器优化导致的指令重排序。值得注意的是:new一个对象不是原子性操作,可以分为三步骤:

  • 为对象分配内存空间

  • 初始化对象

  • 将对象指向分配好的内存空间

对于上面的执行,如果1和3先执行了(假设2还没有完成),在第一层if外的线程这时候判断不为null,这时候就会直接返回该对象,但是这个对象还没进行初始化!之后使用就会导致线程安全问题。
通过volatile就可以确保这3步骤必须执行完(无论顺序如何,最终都会执行完),外面的线程才可以执行,这时候就保证了该对象的完整性。
volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量

3、特点

  • 私有构造方法
  • 提供该类的静态变量,不是马上创建
  • 提供公开的获取唯一对象的静态方法

4、优缺点

  • 优点:使用对象时,对象才创建,所以不会提前占用内存,内存占用小
  • 缺点:首次使用对象时,需要等待对象的创建,而且每次都需要判断对象是否为空,运行效率较低
    以时间换空间

四、不讲武德的小伙子——反射和序列化

想象一下,有人直接把你作业偷掉,复制一份给女神,那你岂不是做了无用功?还真有这种不讲武德的小伙子——反射和序列化

反射

利用反射,强制访问类的私有构造器,去创建另一个对象

public static void main(String[] args) {
    // 获取类的显式构造器
    Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
    // 可访问私有构造器
    construct.setAccessible(true); 
    // 利用反射构造新对象
    Singleton obj1 = construct.newInstance(); 
    // 通过正常方式获取单例对象
    Singleton obj2 = Singleton.getInstance(); 
    System.out.println(obj1 == obj2); // false
}

序列化

public static void main(String[] args) {
    // 创建输出流
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
    // 将单例对象写到文件中
    oos.writeObject(Singleton.getInstance());
    // 从文件中读取单例对象
    File file = new File("Singleton.file");
    ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
    Singleton newInstance = (Singleton) ois.readObject();
    // 判断是否是同一个对象
    System.out.println(newInstance == Singleton.getInstance()); // false
}

那怎么防这两个老六呢?

五、居家必备好助手——枚举

这种方法仅适用jdk1.5之后

public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        System.out.println("这是枚举类型的单例模式!");
    }
}
  • 优势1:代码对比饿汉式与懒汉式来说,更加地简洁
  • 优势2:它不需要做任何额外的操作去保证对象单一性与线程安全性
  • 优势3:使用枚举可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象,破坏单例模式。

六、应用场景

  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 配置对象、数据库的连接池等。
  • 当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • Spring的IOC容器里面的Bean默认为单例模式。

总结

世界上没有两片完全一样的雪花,单例模式是一种比较朴素的思想,在程序设计上分为懒汉模式和饿汉模式还有枚举类,该模式在Spring等框架也广泛应用。

参考文章

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值