23种设计模式

这里写目录标题

  • 1.什么是设计模式
  • 2.创建型模式
    • 2.1 单例模式
      • 2.1.1 饿汉式
      • 2.1.2 懒汉式
    • 2.2 工厂模式

1.什么是设计模式

设计模式是解决软件开发某些特定问题而提出的一些解决方案, 也可以理解为解决特定问题的思路, 通过设计模式可以增强代码的可重用性、可扩充性、可维护性、灵活性等等. 使用设计模式的目的是实现代码的解耦高内聚

设计模式的三大类以及关键点
23种设计模式_第1张图片

2.创建型模式

对象实例化的模式,创建型模式用于解耦对象的实例化过程。

  • 单例模式:某个类智能有一个实例,提供一个全局的访问点。
  • 工厂模式:一个工厂类根据传入的参量决定创建出哪一种产品类的实例。
  • 抽象工厂模式:创建相关或依赖对象的家族,而无需明确指定具体类。
  • 建造者模式:封装一个复杂对象的创建过程,并可以按步骤构造。
  • 原型模式:通过复制现有的实例来创建新的实例。

2.1 单例模式

一、单例模式常见的三种实现方法

  • 饿汉式
  • 懒汉式
  • 登记式

二、单例模式的特点

  1. 单例类只能有一个实例
  2. 单例类必须自己创建这个实例
  3. 单例类必须向整个系统提供这个实例

三、单例模式的应用场景
单例模式确保某个类有且只有一个实例, 且自行实例化后向整个系统提供这个实例.
在计算机系统中, 线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计为单例. 这些应用都或多或少的具有资源管理器的功能, 每台计算机可以有若干个打印机, 但只能有一个PrinterSpooler, 避免两个打印作业同时输出到打印机中. 总而言之, 单例模式就是为了避免不一致的状态.

2.1.1 饿汉式

优点: 没有任何锁, 执行效率高, 用户体验比懒汉式更好

缺点: 类加载的时候就初始化, 不管用不用都占用内存空间

建议: 适用于单例模式较少的场景,
如果我们在项目启动后, 一定会加载到类, 那么用饿汉式既简单又实用,
如果我们是写一些工具类, 则优先考虑懒汉模式, 可以避免提前被加载到内存, 占用系统资源

为什么设计成static(静态成员)
保证只初始化一次, 且在类加载的时候进行初始化(与对象无关)

java程序运行时, 必须经过编译这个过程

  • 首先编译器将后缀名为.java的源文件进行编译, 最终生成后缀名为.class的字节码文件
  • 然后JVM将编译好的字节码文件加载到内存中(这个过程被称为类加载, 通过类加载器完成), 而静态成员就是在类加载的过程中被初始化的
  • JVM对每个类只会加载一次, 也就是说静态成员也只会被初始化一次
public class Singleton {
    private final static Singleton singleton = new Singleton();

    public Singleton getInstance() {
        return singleton;
    }
}

为什么设计为final
既然是单例, 就不支持被重新赋值(重新赋值后, 就是另一个对象了)
23种设计模式_第2张图片

2.1.2 懒汉式

一、线程不安全的懒汉式
为什么要将实例设置为static, 因为getInstance方法是static(静态方法直接调用)

public class Singleton {

	private Singleton() {}
	
    private static Singleton singleton;

    public static Singleton getInstance() {
        if (singleton == null) {			// 代码1
            singleton = new Singleton();	// 代码2
        }
        return singleton;
    }
}

Singleton通过将构造方法限定为private避免了类在外部被实例化, Singleton的唯一实例只能通过getInstance()方法访问

但是上述的懒汉式并没有考虑线程安全问题, 它是线程不安全的

  1. 线程A执行到了代码1的位置, 还未进行判断
  2. 线程B通过了判断, 执行到了代码2的位置, 正在new Singleton(), 但还没有new结束
  3. 线程A看到single还未被实例化, 也会执行代码2, 再次实例化对象
  4. 两个线程都实例化对象, 无法保证单例模式

二、在getInstance方法上加同步

public class Singleton {

	private Singleton() {}
    
    private static Singleton single;

    //静态工厂方法
    public synchronized  static Singleton getInstance() {
        if (single == null) {
            single = new Singleton();
        }
        return single;
    }
}

存在的问题:

  1. 由于对getInstance()方法做了同步处理,synchronized将导致性能开销。
  2. 如果getInstance()方法被多个线程频繁的调用,将会导致程序执行性能的下降。
  3. 如果getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。

三、引入双重检查锁定
由于synchronized存在巨大的性能开销。因此,人们想出了一个“聪明”的技巧:双重检查锁定【Double-Checked Locking】。人们想通过双重检查锁定来降低同步的开销。

public class Singleton {
    
    private static Singleton single = null;

    //静态工厂方法
    public static Singleton getInstance() {
        if (single == null) {							// 1处、第一次检查
        	// 只有当单例对象为空时, 才实例化对象(同步锁)
        	synchronized(Singleton.class) {				// 2处、加锁
        		if (single == null) {					// 3处、第二次检查
        			single = new Singleton();			// 4处、实例化对象【这里会出问题的】
        		}
        	}
        }
        return single;
    }
}

在1处,如果是第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。
在2处,如果多个线程试图在同一时间创建对象时,这里有同步代码块,会通过加锁来保证只有一个线程能创建对象。
在3处,获得锁的线程,会二次检查这个对象是否已经被初始化。
在4处,对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。

问题所在:
一切都是那么的美好,但是有一种情况,在线程执行到1处,读取到instance不为null时;在4处的线程正在初始化实例instance,但是instance引用的对象有可能还没有完成初始化,因为发生了指令重排。
4处因为指令重排,引发的1处拿到的实例在使用的时候发生空指针的问题。(拿到的对象是还未完成初始化的对象)

四、通过volatile解决

public class Singleton {
    
    private static volatile Singleton single = null;

    //静态工厂方法
    public static Singleton getInstance() {
        if (single == null) {							// 1处、第一次检查
        	// 只有当单例对象为空时, 才实例化对象(同步锁)
        	synchronized(Singleton.class) {				// 2处、加锁
        		if (single == null) {					// 3处、第二次检查
        			single = new Singleton();			// 4处、实例化对象【这里会出问题的】
        		}
        	}
        }
        return single;
    }
}

volatile 可以禁止single = new Singleton();过程中的指令重排,从而实现线程的安全。
项目中用的双重检查锁定是怎么回事

2.2 工厂模式

你可能感兴趣的:(设计模式,java,单例模式)