创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
整挺好:https://blog.csdn.net/carson_ho/article/details/54910518
单例模式
简介
介绍
单例模式(Singleton),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
实现思路
一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名 称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们 还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
注意点
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例, 这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
优缺点
优点:
- 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例。
- 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
- 提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。
- 避免对共享资源的多重占用。
缺点:
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
适用场景
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
以下都是单例模式的经典使用场景:
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
- 控制资源的情况下,方便资源之间的互相通信。如线程池等。
应用场景举例:
- 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件。
- Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
- windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
- 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系。
- HttpApplication 也是单例的典型应用。
实现方式
饿汉式
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的
1 | public class Singleton { |
缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用
懒汉式,线程不安全
1 | public class Singleton { |
这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例
懒汉式,线程安全
为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)
1 | public static synchronized Singleton getInstance() { |
虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁
双重检验锁
1 | public class Singleton { |
称其为双重检查锁,是因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了
而 instance = new Singleton() 这句,并非是一个原子操作,事实上在 JVM 中这句话做了下面 3 件事:
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将 instance 对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错
所以需要将 instance 变量声明成 volatile 「禁止重排序」
静态内部类
1 | public class Singleton { |
静态内部类与外部类没有什么关系,外部类加载的时候,内部类不会被加载,静态内部类只是调用的时候用了外部类的名字而已,所以即使 Singleton 类被加载也不会创建单例对象
静态内部类 SingletonHolder 只有在 getInstance() 方法第一次被调用时,才会被加载,从而初始化它的静态域(创建 Singleton 的实例),因此该种方式实现了懒汉式的单例模式
不仅如此,根据 JVM 本身机制,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性
同时不用 synchronized,所以没有性能缺陷
1 | public class Singleton { |
这个写法也是利用类的静态变量的唯一性,不过和上面的写法相比,不能实现懒加载
Enum
用枚举写单例最大的优点是简单
1 | public enum Singleton { |
以上是一个单例枚举的例子,要获取该实例只需要 Singleton.INSTANCE,并且此种方式可以保证该单例线程安全、防反射攻击、防止序列化生成新的实例
反编译后的类:
1 | public abstract class Singleton extends Enum { |
看了这个类的真身后,可以知道:
- 枚举类实现其实省略了 private 类型的构造函数
- 枚举类的域其实是相应的 enum 类型的一个实例对象
- 枚举类的域会在 static 方法块中被实例化,也就是说在 enum 被类加载器加载时被实例化,并非懒加载
- enum 是 abstract 类,所以没法实例化,反射也无能为力
- 关于线程安全的保证,其实是通过类加载机制来保证的,INSTANCE 是在 static 块中实例化的,JVM 加载类的过程显然是线程安全的
- 而枚举可以反序列化是因为 Enum 实现了 readResolve 方法
对于一个标准的 enum 单例模式,最优秀的写法还是实现接口的形式:
1 | // 定义单例模式中需要完成的代码逻辑 |
破坏单例模式的三种方式
- 反射
- 序列化
- 克隆
当单例类被多个类加载器加载,如何还能保持单例
如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些 servlet 容器对每个 servlet 使用完全不同的类装载器,这样的话如果有两个 servlet 访问一个单例类,它们就都会有各自的实例
基于同样的原因,分布式系统和集群系统也都可能出现单例失效的情况
解决方法:用多个类加载器的父类来加载单例类
1 | private static Class getClass(String classname) throws ClassNotFoundException { |
单例类防止反序列化
1 | public class Singleton implements java.io.Serializable { |
readResolve() 方法可以理解反序列化过程的出口,就是在反序列化完成得到对象前,把这个对象换成我们确定好的那个。
反射破坏单例原则
在单例模式中,只对外提供工厂方法(获取单例),并私有化构造函数,来防止外面多余的创建。对于一般的外部调用来说,私有构造函数已经很安全了。但是一些特权用户可以通过反射来访问私有构造函数,然后打开访问权限 setAccessible(true),就可以访问私有构造函数了,这样破坏了单例的私有构造函数保护,创建了一个新的实例。如果要防御这样的反射侵入,可以修改构造函数,加上第二次实例化的检查,当发生创建第二个单例的请求时会抛出异常。
1 | private static int cntInstance = 0; |
作者:杰哥长得帅
链接:https://www.jianshu.com/p/ee110d76c42b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
模板方式模式
自己根据 AQS 重写了一个可重入,一个不可重入的显示锁。
1 | /** |
代理设计模式
动态代理
为何动态代理只能代理接口
其实我觉得完全可以代理类,如果你说 ”是因为Proxy$0已经继承了Proxy,java里面单继承所以导致只能代理接口“,那我就得否认你了,因为其实完全可以做到直接让 ”Proxy去代理“,所以,我觉得人家这么设计的原因只有一个:
看了这么多分析的,我觉得这样设计的初衷就是为了套用代理模式模板和基于接口编程规范的
静态代理中就是实现接口,再在代理类set进实现类的引用,调用真正代理的方法
就算他不继承Proxy,也不会继承被代理的实现类 ,而是必须有一个代理类的接口去实现它,这是设计的初衷
作者:Forward
链接:https://www.zhihu.com/question/62201338/answer/904553916
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
动态代理的过程是什么
代理的过程在 Proxy.newProxyInstance() 方法中,程序先是进行了验证、优化、缓存、同步、生成字节码和显式类加载等操作,最后调用的 sum.misc.ProxyGenerator.generateProxyClass() 来完成生成字节码的动作,这个方法可以在运行时产生一个描述代理类的字节码数组byte[]。
代理类的实现代码也很简单,它为每一个传入接口中的每一个方法都进行了相应的实现,调用的方法都是在执行 InvocationHandler.invoke() 中的代理逻辑。
动态代理的好处是什么
实现了可以在原始类和接口还未知的情况下,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以灵活的重用于不同的应用场景中了。
什么叫静态代理
静态代理,在编译期间就已经确定了要代理的类和接口,是写死了的。
1 | nterface Person{ |
Demo
1 | package 设计模式; |