GOF设计模式入门&&单例模式笔记(java版)

2019-04-14 16:44发布

设计模式主要分三类:

创建者模式

顾名思义,创建者模式帮助我们创建对象。包括:单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式。

结构型模式

适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。

行为型模式

模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模
式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。

单例模式

核心作用

保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。

常见应用场景:

  • Windows的Task Manager(任务管理器)就是很典型的单例模式
  • windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  • 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
  • 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作
    ,否则内容不好追加。
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
  • 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  • Application 也是单例的典型应用(Servlet编程中会涉及到)
  • 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
  • 在servlet编程中,每个Servlet也是单例
  • 在spring MVC框架/struts1框架中,控制器对象也是单例
单例模式的优点
  1. 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
  2. 单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理

常见的五种单例模式实现方式

  • 主要:
    饿汉式:线程安全,调用效率高。 但是,不能延时加载。
    懒汉式:线程安全,调用效率不高。 但是,可以延时加载。
  • 其他:
    双重检测锁式:由于JVM底层内部模型原因,偶尔会出问题。不建议使用
    静态内部类式:线程安全,调用效率高。 但是,可以延时加载
    枚举单例:线程安全,调用效率高,不能延时加载
饿汉式实现(单例对象立即加载)
  1. 饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字。
  2. 问题:如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!
package com.qianyu.singleton; /** * 饿汉式实现单例模式 * * @author lijing * */ public class SingletonDemo01 { // 私有构造器,外界不能通过构造函数创建对象 private SingletonDemo01() { } // 类初始化时,立即加载这个对象(没有延时加载的优势)。加载类时,天然的是线程安全的! public static SingletonDemo01 instance = new SingletonDemo01(); // 方法没有同步,调用效率高! public static SingletonDemo01 getInstance() { return instance; } } 测试代码:
在这里插入图片描述
懒汉式实现(单例对象延迟加载)
要点:lazy load! 延迟加载, 懒加载! 真正用的时候才加载!
问题:如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费! package com.qianyu.singleton; /** * 懒汉式实现单例模式 * * @author lijing * */ public class SingletonDemo02 { // 私有构造器,外界不能通过构造函数创建对象 private SingletonDemo02() { } // 类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)。 public static SingletonDemo02 instance; // 方法同步,防止出现多线程时出现创建多个对象的情况,但是调用效率低! public static synchronized SingletonDemo02 getInstance() { if (instance == null) { instance = new SingletonDemo02(); } return instance; } } 测试代码:
在这里插入图片描述
双重检测锁实现
  • 这个模式将同步内容下方到if内部,提高了执行的效率不必每次获取对象时都进行同步,只有第一次才同步创建了以后就没必要了。
  • 问题:由于编译器优化原因和JVM底层内部模型原因,偶尔会出问题。不建议使用。
package com.qianyu.singleton; /** * 双重检测锁实现单例模式 * * @author lijing * */ public class SingletonDemo03 { // 私有构造器,外界不能通过构造函数创建对象 private SingletonDemo03() { } // 类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)。 public static SingletonDemo03 instance; public static SingletonDemo03 getInstance() { if (instance == null) { synchronized (SingletonDemo03.class) { if (instance == null) { instance = new SingletonDemo03(); } } } return instance; } }
静态内部类实现(也是一种懒加载方式)
要点:
  1. 外部类没有static属性,则不会像饿汉式那样立即加载对象。
  2. 只有真正调用getInstance(),才会加载静态内部类。加载类时是线程 安全的。 instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性.
  3. 兼备了并发高效调用和延迟加载的优势!
package com.qianyu.singleton; /** * 静态内部类实现单例模式 * * @author lijing * */ public class SingletonDemo04 { // 私有构造器,外界不能通过构造函数创建对象 private SingletonDemo04() { } static class Instance{ private static final SingletonDemo04 instance=new SingletonDemo04(); } public static SingletonDemo04 getInstance() { return Instance.instance; } }
使用枚举实现单例模式
关于枚举类型,可以参考这篇文章:https://www.jianshu.com/p/46dbd930f6a2
优点:
  1. 实现简单
  2. 枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
缺点:
  1. 无延迟加载
package com.qianyu.singleton; /** * 使用枚举实现单例模式 * * @author lijing * */ public enum SingletonDemo05 { // 定义一个枚举元素,它就代表了Singleton的一个实例。 INSTANCE; /** * 单例可以有自己的操作 */ public void singletonOperation() { // 功能处理 } } 测试代码:
在这里插入图片描述

反射破解上面几种单例(不包括枚举式)

/** * 使用反射破解单例,以懒汉式为例 * * @throws Exception */ public static void test01() throws Exception { Class<SingletonDemo02> clazz = (Class<SingletonDemo02>) Class.forName("com.qianyu.singleton.SingletonDemo02"); Constructor<SingletonDemo02> constructor = clazz.getDeclaredConstructor(null); // 如果要访问私有成员,跳过权限检查 constructor.setAccessible(true); SingletonDemo02 s1 = constructor.newInstance(); SingletonDemo02 s2 = constructor.newInstance(); System.out.println(s1); System.out.println(s2); } 测试结果:
在这里插入图片描述
可以看出,s1和s2不是一个对象。
解决方法:可以在构造函数中手动添加抛出异常控制 private SingletonDemo02() throws Exception { if (instance != null) { throw new Exception("只能创建一个对象"); } } 测试类: package com.qianyu.gof; import java.lang.reflect.Constructor; /** * @author lijing * @date 2019-04-08-13:45 * @discroption 使用反射破解单例 */ public class Test { public static void main(String[] args) throws Exception { SingletonDemo02 s = SingletonDemo02.getInstance(); System.out.println(s); Class<SingletonDemo02> clazz = (Class<SingletonDemo02>) Class.forName("com.qianyu.gof.SingletonDemo02"); Constructor<SingletonDemo02> c = clazz.getDeclaredConstructor(null); c.setAccessible(true); SingletonDemo02 s1 = c.newInstance(); SingletonDemo02 s2 = c.newInstance(); System.out.println(s1); System.out.println(s2); } } 运行结果:
在这里插入图片描述
可以发现,在使用反射调用构造器时会抛出异常,注意要先调用getInstance()方法是单例中静态成员不为空。

反序列化破解上面几种单例(不包括枚举式)

测试类: package com.qianyu.gof; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; /** * @author lijing * @date 2019-04-08-13:45 * @discroption 使用反序列化的方式破解单例 */ public class Test { public static void main(String[] args) throws Exception { SingletonDemo02 s1 = SingletonDemo02.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("f:/1.obj")); oos.writeObject(s1); oos.close(); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("f:/1.obj")); Object s2 = ois.readObject(); ois.close(); ois.close(); System.out.println(s1); System.out.println(s2); } } 运行结果:
在这里插入图片描述
可以发现,s1和s2是两个不同对象。
解决方法:可以通过定义readResolve()防止获得不同对象。
原理:反序列化时,如果对象所在类定义了readResolve(),(实际是一种回调),定义返回哪个对象。完整代码如下: package com.qianyu.gof; import java.io.ObjectStreamException; import java.io.Serializable; /** * @author lijing * @date 2019-04-08-13:35 * @discroption 懒汉式单例模式 */ public class SingletonDemo02 implements Serializable { // 类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)。 public static SingletonDemo02 instance; // 私有构造器,外界不能通过构造函数创建对象 private SingletonDemo02() throws Exception { // 如果这个类中静态成员instance不为空,说明已经创建过该对象,就抛出异常 if (instance != null) { throw new Exception("只能创建一个对象"); } } // 方法同步,防止出现多线程时出现创建多个对象的情况,但是调用效率低! public static synchronized SingletonDemo02 getInstance() throws Exception { if (instance == null) { instance = new SingletonDemo02(); } return instance; } private Object readResolve() throws ObjectStreamException{ return instance; } } 再次运行测试代码,s1和s2为同一对象。
在这里插入图片描述

常见的五种单例模式在多线程环境下的效率测试

在这里插入图片描述
下面是测试代码,介绍一个类:CountDownLatch:同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
  • countDown() 当前线程调此方法,则计数减一(建议放在 finally里执行)
  • await(), 调用此方法会一直阻塞当前线程,直到计时器的值为0
public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); final CountDownLatch cd = new CountDownLatch(10); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 1000000; j++) { SingletonDemo01.getInstance(); } cd.countDown(); } }).start(); } cd.await(); long end = System.currentTimeMillis(); System.out.println(end - start); }

总结

  • 饿汉式:线程安全,调用效率高,但是不能延时加载
  • 懒汉式:线程安全,调用效率不高,可以延时加载
  • 双重检测锁式:由于JVM底层内部模型原因,偶尔会出问题不建议使用
  • 静态内部类式:线程安全,调用效率高,可以延时加载
  • 枚举式:线程安全,调用效率高,不能延时加载,而且可以天然防止反射和反序列化漏洞!
    如何选用?
  1. 单例对象占用资源少,不需要延时加载:枚举式好于饿汉式。
  2. 单例对象占用资源大,需要延时加载:静态内部类式好于懒汉式。