JAVA设计模式第二讲:创建型设计模式(上)

简介: JAVA设计模式第二讲:创建型设计模式(上)

7、常用的设计模式(代表了最佳实践 共23种,常用的14种)

掌握设计模式的五个层次

第一层:刚开始学编程不久,听说过什么是设计模式;

第二层:有长时间的编程经验,自己写了很多代码,其中用到了设计模式,但是自己却不知道;

第三层:学习过了设计模式,发现自己已经在使用了,并且发现一些新的模式挺好用的;✅

第四层:阅读了很多别人写的源码和框架, 在其中看到别人设计模式,并且能够领会设计模式的精妙和带来的好处

第五层: 代码写着写着,自己都没有意识到使用了设计模式,并且熟练的写了出来

总体来说设计模式分为三大类

创建型模式(对对象创建过程中各种问题和解决方案的总结)

  • 共5种:单例模式,工厂方法模式(抽象工厂模式),建造者模式,原型模式(不常用)

结构型模式(关注于类/对象 继承/组合方式)

  • 共7种:代理模式,桥接模式,适配器模式,装饰器模式,外观模式(不常用),组合模式(不常用),享元模式(不常用)

行为型模式(是从类或对象之间交互/职责划分等角度总结的模式)

  • 共11种:观察者模式,模板方法模式,策略模式,迭代器模式,责任链模式,状态模式,命令模式(不常用),备忘录模式(不常用),访问者模式(不常用),中介者模式(不常用),解释器模式(不常用)
  • 总结
  • 设计模式要干的事情就是解耦。
    创建型模式是将创建和使用代码解耦结构型模式是将不同功能代码解耦行为型模式是将不同的行为代码解耦

8、创建型设计模式

创建型设计模式包括:单例模式,工厂方法模式(抽象工厂模式),建造者模式,原型模式(不常用)。它主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码

8.1、单例设计模式一共有几种实现方式?请分别用代码实现,并说明各个实现方式的优点和缺点?

单例模式用来创建全局唯一的对象。

定义:一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。单例有几种经典的实现方式,它们分别是:饿汉式(两种)、懒汉式(三种)、双重检测、静态内部类、枚举(最佳实践)。

使用场景:①需要频繁的进行创建和销毁的对象、②创建对象时耗时过多或耗费资源过多(即:重量级对象), 但又经常用到的对象、③工具类对象、④频繁访问数据库或文件的对象(比如数据源、 session工厂等)

Demo1、饿汉式:(关键词:静态常量)

public Class singleton{
   // 1、私有化构造函数 
     private Singleton() {}
     // 2、内部直接创建对象 
     public static singleton instance = new singleton();
     // 3、提供公有静态方法,返回对象实例 
     public static Singleton getInstance() {
        return instance;
     }
}
优点:写法简单,避免线程同步问题
缺点:没有懒加载,可能造成内存浪费  不推荐. 
使用场景:耗时的初始化操作,提前到程序启动时完成

Demo2、饿汉式(静态代码块)

public Class singleton{
   // 1、私有化构造函数 
     private Singleton() {}
    //2.本类内部创建对象实例
  private static Singleton instance;
  static { 
    // 在静态代码块中,创建单例对象
    instance = new Singleton();
  }
  //3. 提供一个公有的静态方法,返回实例对象
  public static Singleton getInstance() {
    return instance;
  }
}
优点:写法简单,避免线程同步问题
缺点:没有懒加载,可能造成内存浪费   不推荐

Demo3、懒汉式(线程不安全)

class Singleton {
  private static Singleton instance;
  private Singleton() {}
  //提供一个静态的公有方法,当使用到该方法时,才去创建 instance
  public static Singleton getInstance() {
    if(instance == null) {
      instance = new Singleton();
    }
    return instance;
  }
}
优点:起到了懒加载的效果
缺点:只能在单线程环境使用, 不要使用

Action1:为什么懒汉单例在多线程环境下会有线程安全问题呢?

在多线程环境下,显然不能保证单例。

  • 如果多个线程能够同时进入 if (instance == null) ,并且此时 instance 为 null,那么会有多个线程执行 instance = new SingletonDemo(); 语句,这将导致多次实例化 instance。
public class SingletonDemo {
    private static SingletonDemo instance = null;
    private SingletonDemo () {
        System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
    }
    public static SingletonDemo getInstance() {
        if(instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

输出结果:

4  我是构造方法SingletonDemo
2  我是构造方法SingletonDemo
5  我是构造方法SingletonDemo
6  我是构造方法SingletonDemo
0  我是构造方法SingletonDemo
3  我是构造方法SingletonDemo
1  我是构造方法SingletonDemo

解决方法之一:

  • synchronized修饰方法getInstance(),但它属重量级同步机制,使用时慎重。

Demo4、懒汉式(线程安全)

class Singleton {
  private static Singleton instance;
  private Singleton() {}
  //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
  public static synchronized Singleton getInstance() {
    if(instance == null) {
      instance = new Singleton();
    }
    return instance;
  }
}
优点:起到了懒加载的效果,解决了线程安全问题
缺点:效率低,不推荐

Demo5、懒汉式(线程安全,同步代码块)

class Singleton {
  private static Singleton singleton;
  private Singleton() {}
  //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
  public static Singleton getInstance() {
    if(singleton == null) {
    synchronized (Singleton.class) {
      singleton = new Singleton();
    }
    return singleton;
  }
}
不推荐使用,并不能起到线程同步的作用

Demo6、双重锁校验 可以应用于连接池的使用中

  • DCL中volatile解析
  • 原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance 的引用对象可能没有完成初始化instance = new SingletonDemo();可以分为以下3步完成(伪代码):
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null
  • 步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
  • 但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
  • 所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
memory = allocate(); //1.分配对象内存空间
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! =null,但是对象还没有初始化完成!
instance(memory);//2.初始化对象
public Class singleton{
    private static volatile Singleton instance;
    private Singleton() {}
    //提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
    //同时保证了效率, 推荐使用
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (Singleton.class) {
                if(instance == null) {
                   instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
// 优点:Double-Check机制保证线程安全,
// 加锁操作只需要对实例化那部分的代码进行,只有当 instance 没有被实例化时,才需要进行加锁。效率高   
// 因此推荐使用

问题1:如果只使用了一个 if 语句。在 instance == null 的情况下,如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 instance = new Singleton(); 这条语句,只是先后的问题,那么就会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 语句。

问题2:instance 采用 volatile 关键字修饰也是很有必要的。instance = new Singleton(); 这段代码其实是分为三步执行。

  1. 分配内存空间
  2. 初始化对象
  3. 将 instance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1>3>2,这在单线程情况下自然是没有问题。但如果是多线程下,有可能获得是一个还没有被初始化的实例,以致于程序出错。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行

Demo7、静态内部类

class Singleton {
  private static Singleton instance;
  //构造器私有化
  private Singleton() {}
  //写一个静态内部类,该类中有一个静态属性 Singleton
  private static class SingletonInstance {
    private static final Singleton INSTANCE = new Singleton(); 
  }
  //提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
  public static Singleton getInstance() {
    return SingletonInstance.INSTANCE;
  }
}
优点:JVM保证了线程安全,类的静态属性只会在第一次加载类的时候初始化,效率高,推荐

Demo8、枚举 – 单例实现的最佳实践

enum Singleton {
  INSTANCE; //属性
  public void sayOK() {
    System.out.println("ok");
  }
}
// main方法来测试
public static void main(String[] args) {
  Singleton instance = Singleton.INSTANCE;
  instance.sayOK();
}
优点:使用枚举,可以实现单例,避免了多线程同步问题,还能防止反序列化重新创建新的对象,推荐

补充Demo9:java核心类库 Runtime 的单例实现(饿汉式)

  • 每个 Java 应用在运行时会启动一个 JVM 进程,每个 JVM 进程都只对应一个 Runtime 实例,用于查看 JVM 状态以及控制 JVM 行为。进程内唯一,所以比较适合设计为单例。
//静态实例被声明为final,一定程度上保证了实例不被篡改
public class Runtime {
  private Runtime(){}
  private static final Runtime currentRuntime = new Runtime();
  private static Version version;
  public static Runtime getRuntime(){
      return currentRuntime;
  }
  public void addShutdownHook(Thread hook) {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(new RuntimePermission("shutdownHooks"));
    }
    ApplicationShutdownHooks.add(hook);
  }
}

Demo10: 获取 spi 单例

public class SpiProviderSelector {
    private static volatile SpiProviderSelector instance = null;
    private SpiProviderSelector(){}
    /* 获取单例*/
    public static SpiProviderSelector getInstance() {
        if(instance == null){
            synchronized (SpiProviderSelector.class){
                if(instance == null){
                    instance = new SpiProviderSelector();
                }
            }
        }
        return instance;
    }
}

单例模式缺点

1、单例对 OOP 特性的支持不友好

  • 对继承、多态特性支持不友好

2、单例对代码的可测试性不友好

  • 硬编码方式,无法实现 mock 替换

3、单例不支持有参数的构造函数

Demo11:怎么在单例模式中给对象初始化数据?

public class Singleton {
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;
  private Singleton(int paramA, int paramB) {
    this.paramA = paramA;
    this.paramB = paramB;
  } 
  public static Singleton getInstance() {
    if (instance == null) {
      throw new RuntimeException("Run init() first.");
    }
    return instance;
  } 
  public synchronized static Singleton init(int paramA, int paramB) {
    if (instance != null){
      throw new RuntimeException("Singleton has been created!");
    }
    instance = new Singleton(paramA, paramB);
    return instance;
  }
} 
// 先init,再使用 getInstance()
Singleton.init(10, 50); 
Singleton singleton = Singleton.getInstance()

单例模式的替代方案?

  • 通过工厂模式、IOC 容器来保证全局唯一性。

Action2:请问 Spring下的 bean 单例模式与设计模式(GOF)中的单例模式区别?** 可以作为面试题

  • 它们关联的环境不同,单例模式是指在一个JVM进程中仅有一个实例,不管在程序中的何处获取实例,始终都返回同一个对象。
    Spring单例是指一个Spring Bean容器(ApplicationContext)中仅有一个实例。

Action3:单例模式实现方式总结

Action4:为什么说 Enum 实现单例模式是最佳实践

利用Enum实现单例模式

/* 使用枚举实现单例 */
public enum EnumSingleton {
  // 唯一的实例对象
    INSTANCE; 
    // 单例对象的属性对象
    private Object obj = new Object();
    public Object getObj() {
        return obj;
    }
    // 单例提供的对外服务。
    public Object getFactoryService() {
        return new Object();
    }
}

反编译代码

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingleton.java
public final class EnumSingleton extends Enum {
    public static EnumSingleton[] values() {
        return (EnumSingleton[])$VALUES.clone();
    }
    public static EnumSingleton valueOf(String name) {
        return (EnumSingleton)Enum.valueOf(EnumSingleton, name);
    }
    // 无法通过new来随意创建对象,构造函数为private.
    private EnumSingleton(String s, int i) {
        super(s, i);
        obj = new Object();
    }
    public Object getObj() {
        return obj;
    }
    public Object getFactoryService() {
        return new Object();
    }
    // 提供获取唯一实例对象的方法,通常是getInstance
    // 也可以直接获取到INSTANCE,但是获取到的都是一个对象
    public static final EnumSingleton INSTANCE;
    private Object obj;
    private static final EnumSingleton $VALUES[];
    // 静态代码中实例化对象,多线程并发的情况下保证唯一,属于饿汉模式
    static {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

从反编译代码中我们可以看到 Enum 的特点

  • 枚举本质上是个final类
  • 定义的枚举值实际上就是一个枚举类的不可变对象(比如这里的INSTANCE)
  • 在Enum类加载的时候,就已经实例化了这个对象
  • 无法通过 new 来创建枚举对象

Enum实现单例模式的几个关键点验证

1.避免反射创建单例对象(反射攻击)

枚举的私有构造函数如下所示:

private EnumSingleton(String s, int i)

尝试利用反射创建对象

/**
 * 反射攻击。
 * 由于Enum天然的不允许反射创建实例,所以可以完美的防范反射攻击。
 */
private static void reflectionAttack() {
    try {
        Constructor con = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
        con.setAccessible(true);
        // 反射新建对象以破坏单例
        Object obj = con.newInstance("INSTANCE", 0);
        System.out.println(obj);
        System.out.println(EnumSingleton.getInstance());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

会抛出异常"java.lang.IllegalArgumentException: Cannot reflectively create enum objects"。如下所示:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:416)
    at com.ws.pattern.singleton.EnumSingletonAppMain.reflectionAttack(EnumSingletonAppMain.java:43)
    at com.ws.pattern.singleton.EnumSingletonAppMain.main(EnumSingletonAppMain.java:9)

从异常可以看出来,newInstance抛出了异常。推测Java反射是不允许创建Enum对象的,看看源码Constructor.java中的newInstance方法,存在处理Enum类型实例化的一行判断代码 if ((clazz.getModifiers() & Modifier.ENUM) != 0),满足这个条件就抛出异常。newInstance的JDK代码如下:

public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

2.避免通过序列化创建单例对象(如果单例类实现了Serializable)(序列化攻击)

  • 序列化对象后,如果执行反序列化,也可以创建一个对象。利用此机制来尝试创建一个新的Enum对象。
/** 序列化攻击
 * 需要在单例类中增加read
 */
private static void serializableAttack() {
    EnumSingleton singleton = EnumSingleton.getInstance();
    System.out.println(singleton);
    try {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./EnumSingleton.out"));
        oos.writeObject(singleton);
        ObjectInputStream ois = new ObjectInputStream((new FileInputStream("./EnumSingleton.out")));
        Object obj = ois.readObject(); // 这里利用反序列化创建对象
        System.out.println(obj);
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

拿到了相同的对象。执行结果如下:

INSTANCE

INSTANCE

跟踪进入ois.readObject(),会进入ObjectInputStream.readObject0方法。其中会解析class的二进制,根据class的文件定义,分别解析不同类型的字段。重点关注case TC_ENUM:如下所示:

switch (tc) {
    case TC_NULL:
        return readNull();
    case TC_REFERENCE:
        return readHandle(unshared);
    case TC_CLASS:
        return readClass(unshared);
    case TC_CLASSDESC:
    case TC_PROXYCLASSDESC:
        return readClassDesc(unshared);
    case TC_STRING:
    case TC_LONGSTRING:
        return checkResolve(readString(unshared));
    case TC_ARRAY:
        return checkResolve(readArray(unshared));
    case TC_ENUM:
        return checkResolve(readEnum(unshared));
    case TC_OBJECT:
        return checkResolve(readOrdinaryObject(unshared));
    case TC_EXCEPTION:
        IOException ex = readFatalException();
        throw new WriteAbortedException("writing aborted", ex);
    case TC_BLOCKDATA:
    case TC_BLOCKDATALONG:
        if (oldMode) {
            bin.setBlockDataMode(true);
            bin.peek();             // force header read
            throw new OptionalDataException(
                bin.currentBlockRemaining());
        } else {
            throw new StreamCorruptedException(
                "unexpected block data");
        }
    case TC_ENDBLOCKDATA:
        if (oldMode) {
            throw new OptionalDataException(true);
        } else {
            throw new StreamCorruptedException(
                "unexpected end of block data");
        }
    default:
        throw new StreamCorruptedException(
            String.format("invalid type code: %02X", tc));
}

进入readEnum方法,重点关注Enum.valueOf方法。如下所示:

/**
 * Reads in and returns enum constant, or null if enum type is
 * unresolvable.  Sets passHandle to enum constant's assigned handle.
 */
private Enum<?> readEnum(boolean unshared) throws IOException {
    if (bin.readByte() != TC_ENUM) {
        throw new InternalError();
    }
    ObjectStreamClass desc = readClassDesc(false);
    if (!desc.isEnum()) {
        throw new InvalidClassException("non-enum class: " + desc);
    }
    int enumHandle = handles.assign(unshared ? unsharedMarker : null);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(enumHandle, resolveEx);
    }
    String name = readString(false);
    Enum<?> result = null;
    Class<?> cl = desc.forClass();
    if (cl != null) {
        try {
            @SuppressWarnings("unchecked")
            // 这里根据 name 和 class拿到 Enum 实例
            Enum<?> en = Enum.valueOf((Class)cl, name);
            result = en;
        } catch (IllegalArgumentException ex) {
            throw (IOException) new InvalidObjectException(
                "enum constant " + name + " does not exist in " +
                cl).initCause(ex);
        }
        if (!unshared) {
            handles.setObject(enumHandle, result);
        }
    }
    handles.finish(enumHandle);
    passHandle = enumHandle;
    return result;
}

再跟进Enum.valueOf方法。代码如下:

public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
     // 从enumConstantDirectory()中根据name获取对象
     T result = enumType.enumConstantDirectory().get(name);
     if (result != null)
         return result;
     if (name == null)
         throw new NullPointerException("Name is null");
     throw new IllegalArgumentException(
         "No enum constant " + enumType.getCanonicalName() + "." + name);
}

enumConstantDirectory()是Class的方法,其本质是从Class.java的enumConstantDirectory属性中获取。代码如下:

private volatile transient Map<String, T> enumConstantDirectory = null;

也就是说,Enum中定义的Enum成员值都被缓存在了这个Map中,Key是成员名称(比如“INSTANCE”),Value就是Enum的成员对象。这样的机制天然保证了取到的Enum对象是唯一的。即使是反序列化,也是一样的。

结论:单例枚举的实现简化了单例模式的创建,利用了枚举特性,使得单例枚举成为最佳实践。

相关文章
|
11天前
|
设计模式 Java
【设计模式】JAVA Design Patterns——Bridge(桥接模式)
【设计模式】JAVA Design Patterns——Bridge(桥接模式)
【设计模式】JAVA Design Patterns——Bridge(桥接模式)
|
6天前
|
设计模式 Java 数据库连接
【重温设计模式】代理模式及其Java示例
【重温设计模式】代理模式及其Java示例
|
10天前
|
设计模式 Java API
【设计模式】JAVA Design Patterns——Combinator(功能模式)
【设计模式】JAVA Design Patterns——Combinator(功能模式)
|
11天前
|
设计模式 监控 Java
【设计模式】JAVA Design Patterns——Circuit Breaker(断路器模式)
【设计模式】JAVA Design Patterns——Circuit Breaker(断路器模式)
|
11天前
|
设计模式 Java 程序员
【设计模式】JAVA Design Patterns——Bytecode(字节码模式)
【设计模式】JAVA Design Patterns——Bytecode(字节码模式)
|
11天前
|
设计模式 算法 Java
【设计模式】JAVA Design Patterns——Builder(构造器模式)
【设计模式】JAVA Design Patterns——Builder(构造器模式)
|
11天前
|
设计模式 Java 容器
【设计模式】JAVA Design Patterns——Async Method Invocation(异步方法调用模式)
【设计模式】JAVA Design Patterns——Async Method Invocation(异步方法调用模式)
|
4天前
|
设计模式 存储 前端开发
Java的mvc设计模式在web开发中应用
Java的mvc设计模式在web开发中应用
|
6天前
|
设计模式 安全 Java
【设计模式】JAVA Design Patterns——Curiously Recurring Template Pattern(奇异递归模板模式)
该文介绍了一种C++的编程技巧——奇异递归模板模式(CRTP),旨在让派生组件能继承基本组件的特定功能。通过示例展示了如何创建一个`Fighter`接口和`MmaFighter`类,其中`MmaFighter`及其子类如`MmaBantamweightFighter`和`MmaHeavyweightFighter`强制类型安全,确保相同重量级的拳手之间才能进行比赛。这种设计避免了不同重量级拳手间的错误匹配,编译时会报错。CRTP适用于处理类型冲突、参数化类方法和限制方法只对相同类型实例生效的情况。
【设计模式】JAVA Design Patterns——Curiously Recurring Template Pattern(奇异递归模板模式)
|
9天前
|
设计模式 Java 数据库
【设计模式】JAVA Design Patterns——Converter(转换器模式)
转换器模式旨在实现不同类型间的双向转换,减少样板代码。它通过通用的Converter类和特定的转换器(如UserConverter)简化实例映射。Converter类包含两个Function对象,用于不同类型的转换,同时提供列表转换方法。当需要在逻辑上对应的类型间转换,或处理DTO、DO时,此模式尤为适用。
【设计模式】JAVA Design Patterns——Converter(转换器模式)