打通设计模式任督二脉 -- 单例模式

x33g5p2x  于2022-05-06 转载在 其他  
字(4.3k)|赞(0)|评价(0)|浏览(464)

前言:想必想了解设计模式的各位都是走开发路线的吧,那么既然是走开发路线的,那么设计模式可是一定要好好学哦。因为它是一种思想,将会贯彻到你的开发过程,同时如果你使用的得当的话,那么你的项目将会十分的流畅且亮点丰富。当然这也是开始学之前的一些废话。总之,脚踏实地好好学。

1、设计模式介绍

我们得先从设计模式的定义出发。哈哈,不知道你们学习东西的流程是什么。反正我学习的一个流程是先要知道这个东西是什么,然后再知道为什么要学习这个东西,然后就是怎样使用这个东西了。这个是学习东西的一个方法和套路,我们需要总结出自己的一个学习新事物的套路来,这样才能事半功倍。

  • 什么是设计模式?

那么什么是设计模式呢?

  1. 官方的定义:软件设计模式,又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。
  2. 说到底,它就是面向对象设计原则的实际运用思想和经验总结,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。

通过上述的定义的描述,大家肯定都对设计模式有了一定的初步的印象了,但是要进行深刻理解的话,这样还是不够的,需要之后对各个设计模式的学习理解和消化。

  • 为什么要使用设计模式呢?
  1. 可以提高程序员的思维能力、编程能力和设计能力。
  2. 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  3. 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

2、单例模式

我们上面简单的介绍了一下设计模式是什么,为什么要学习设计模式,之后我们将从各个设计模式的使用来进行讲解。记住,设计模式是一种思想,我们一定要尽可能地理解它。当然,如果暂时理解不了的话,也不要灰心,慢慢来消化即可。加油吧,骚年!!!

2.1、什么是单例模式?

单例模式(英文名:Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建(所以说如果该类是单例模式的话,那么你使用到的它的每一个对象都是相同的。)。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 单例模式的主要有以下角色:
  1. 单例类只能创建一个实例的类
  2. 访问类:使用单例类

2.2、单例模式实现方式

单例设计模式分类两种(主要区别在于是否在类加载的时候进行相关单实例对象的创建):

饿汉式:类加载就会导致该单实例对象被创建

懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

  • 饿汉式 - 方式1
  1. /**
  2. * @author :小肖
  3. * 饿汉式方式1:静态成员变量
  4. * @date :Created in 2022/5/5 14:58
  5. */
  6. public class Singleton {
  7. // 1. 私有构造
  8. private Singleton() {
  9. }
  10. // 2. 静态成员变量
  11. private static Singleton instance = new Singleton();
  12. // 3. 公有的获取方法
  13. public static Singleton getInstance() {
  14. return instance;
  15. }
  16. }

测试方法

  1. /**
  2. * @author :小肖
  3. * @date :Created in 2022/5/5 15:00
  4. */
  5. public class Test {
  6. public static void main(String[] args) {
  7. // 获取第一个实例
  8. Singleton instance = Singleton.getInstance();
  9. // 获取第二个实例
  10. Singleton instance1 = Singleton.getInstance();
  11. // 比较两个实例是否相等
  12. System.out.println(instance == instance1);
  13. }
  14. }

输出结果

  1. true
  • 饿汉式 - 方式2
  1. /**
  2. * @author :小肖
  3. * 饿汉式方式2:静态代码块
  4. * @date :Created in 2022/5/5 15:05
  5. */
  6. public class Singleton {
  7. // 1. 私有构造
  8. private Singleton() {
  9. }
  10. // 2. 私有静态变量
  11. private static Singleton instance;
  12. // 3. 静态代码块进行创建对象
  13. static {
  14. instance = new Singleton();
  15. }
  16. // 4. 公有的获取方式
  17. public static Singleton getInstance() {
  18. return instance;
  19. }
  20. }

总的说明:我们观察上述的代码可以发现,我们是将对应的类的构造方法私有,进而防止外部进行创建该类对象,同时在类内部直接创建好对象,从而使得对象是单例的,但可能创建的方式不同。但不管是使用静态代码块的方式创建对象,还是直接创建对象,它们都是在类加载的时候就进行了相关对象的创建,如果我们之后没有对其进行使用,或者说之后的代码中并未使用到该对象,那么这样加载的对象就是浪费了相关内存的。这也是饿汉式的缺点所在,那么也就引申出了 懒汉式实现方式

  • 懒汉式 - 线程不安全
  1. /**
  2. * @author :小肖
  3. * 懒汉式实现
  4. * @date :Created in 2022/5/5 15:23
  5. */
  6. public class Singleton {
  7. // 1. 私有构造方法
  8. private Singleton() {
  9. }
  10. // 2. 静态成员变量
  11. private static Singleton instance; // null
  12. // 3. 公有获取方法
  13. public static Singleton getInstance() {
  14. if (instance == null) {
  15. return new Singleton();
  16. }
  17. return instance;
  18. }
  19. }

为什么线程不安全呢?

当线程1读取到当前 instancenull 的时候,那么此时如果就将 cpu 让出给线程2使用的话,那么此时线程2 读取到的当前 instance 也为 null,那么就会导致两个线程创建了多个实例对象,所以在多线程环境下是不安全的。

  • 懒汉式 - 线程安全
  1. /**
  2. * @author :小肖
  3. * 懒汉式实现
  4. * @date :Created in 2022/5/5 15:23
  5. */
  6. public class Singleton {
  7. // 1. 私有构造方法
  8. private Singleton() {
  9. }
  10. // 2. 静态成员变量
  11. private static Singleton instance; // null
  12. // 3. 公有获取方法
  13. public static synchronized Singleton getInstance() {
  14. if (instance == null) {
  15. return new Singleton();
  16. }
  17. return instance;
  18. }
  19. }

虽然这种方式能够解决多线程线程安全问题,但是你要知道 synchronized 是重量级锁,虽然 JVM 底层对它进行了相关的优化,但是还是会带来很大的性能消耗。因为这个方法大多数情况下是读操作,所以只有当需要创建对应的实例对象的时候才需要进行对应的加锁的必要,那么我们可以使用 双重检查锁 + volatile 实现一种更好的单例模式。

  • 懒汉式 - 线程安全(双重检查锁 + volatile)
  1. /**
  2. * @author :小肖
  3. * 懒汉式 双重检查锁 + volatile
  4. * @date :Created in 2022/5/5 15:48
  5. */
  6. public class Singleton {
  7. // 1. 私有构造
  8. private Singleton() {}
  9. // 2. 静态成员变量
  10. private static volatile Singleton instance;
  11. // 3. 公有获取方式
  12. public static Singleton getInstance(){
  13. if(instance == null){
  14. synchronized (Singleton.class){
  15. if(instance == null){
  16. return instance;
  17. }
  18. }
  19. }
  20. return instance;
  21. }
  22. }

这里需要注意,我们为什么要在对应的变量上加 volatile 关键字呢?

因为在多线程的情况下,它还是会造成我们对应的单例对象是空指针的情况,该情况主要是由于虚拟机优化和指令重排造成的。所以说我们可以通过加上 volatile 使得消除这种指令重排。

当然这里还有两种实现单例模式的方式:静态内部类的实现方式、枚举的实现方式。这里我就不一一介绍了。

2.3、单例模式存在的问题

其实单例模式维护的核心就是保证对应的实例对象是唯一的,那么就肯定会有人想方设法的去破环这种唯一性。这就是单例模式所存在的问题了。

破坏方式

  1. 通过序列化和反序列化的方式可以对单例模式进行破坏。
  2. 通过反射机制可以对单例模式进行破坏。

解决方式

  1. Singleton 类中添加 readResolve() 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新 new 出来的对象。
  2. 当通过反射方式调用构造方法进行创建时,如果对应的对象为null的话,直接抛异常。

2.4、JDK源码中使用到的单例模式

Runtime 类就是使用的单例设计模式。

  1. public class Runtime {
  2. private static Runtime currentRuntime = new Runtime();
  3. /**
  4. * Returns the runtime object associated with the current Java application.
  5. * Most of the methods of class <code>Runtime</code> are instance
  6. * methods and must be invoked with respect to the current runtime object.
  7. *
  8. * @return the <code>Runtime</code> object associated with the current
  9. * Java application.
  10. */
  11. public static Runtime getRuntime() {
  12. return currentRuntime;
  13. }
  14. /** Don't let anyone else instantiate this class */
  15. private Runtime() {}
  16. ...
  17. }

从上面源代码中可以看出 Runtime 类使用的是饿汉式(静态属性)方式来实现单例模式的。

编写不易,如果你感觉对你有帮助的话,请你三连支持,后面的文章会一点点更新。

相关文章