最近在看秦小波老师的《设计模式之禅》这本书,里面有句话对我触动挺大的。
设计模式已经诞近 20 年了,其间出版了很多关于它的经典著作,相信大家都能如数家珍。尽管有这么多书,工作 5 年了还不知道什么是策略模式、状态模式、责任链模式的程序员大有人在。
很不幸,我就是这部分人当中的一个。回想起这几年的工作生涯,设计模式不能说没有接触过,但是绝对不多,能想到的随手写出来的几个设计模式也仅限于「单例模式」、「工厂模式」、「建造者模式」、「代理模式」、「装饰模式」。
好吧,我认知比较深的也就这几个模式,说出来都自己感觉脸红,还有很大一部分仅限于听过,说了以后大致知道是什么玩意,没有细细的研究过,正好趁着这个机会,写点文章,给自己补补课,所以这个系列的名字叫「补课」进行时。
至于为什么要选设计模式,因为设计模式这个东西,它是软件行业的经验总结,因此它具有更广泛的适应性,不管你使用什么编程语言,不管你遇到什么业务类型,都需要用到它。
因为它是一个指导思想,学习了它以后,我们可以站在一个更高的层次去赏析程序代码、软件设计、架构,完成一个 Coder 的蜕变。
在古代行军打仗的时候,每支军队都要有一个将军,战场上如何作战,完全需要听将军的指挥,将军怎么说,这个仗就怎么打,每个士兵都知道将军是谁,而不需要在将军前面加上张将军或者是李将军。
既然将军只能有一个,我们需要用程序去实现这个将军的话,也就是一个类只能产生一个将军的对象,不能产生多个,这就是单例模式的要义。
产生一个对象有多重方式,最常见的是直接 new 一个出来,当然,还可以有反射、复制等操作,我们如何来控制一个类只能产生一个对象呢?
最简单的做法是直接在构造函数上动手脚,使用 new 来新建对象的时候,会根据输入的参数调用相应的构造函数,我们如果直接把构造函数设置成 private ,这样就可以做到不允许外部类来访问创建对象,从而保证对象的唯一性。
public class General { // 初始化一个将军 private static final General general = new General(); // 构造函数私有化 private General() { } public static General getInstance() { return general; } public void command() { System.out.println("将军下令,兄弟们跟我上啊!!!"); } }现在我们有了一个将军类,接下来我们实现一个士兵类:
public class Soldier { public static void main(String[] args) { for (int soldiers = 0; soldiers < 5; soldiers++) { General general = General.getInstance(); general.command(); } } }有 5 个士兵收到了将军的命令,跟着将军一起冲锋陷阵,成就一世英名。
单例模式(Singleton Pattern)的定义异常简单:Ensure a class has only one instance, and provide a global point of accessto it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)
优点:
由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
缺点:
单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
注意事项:
在某些有一定并发的场景中,需要注意线程同步的问题,防止创建多个对象,造成未知错误异常。
因为单例模式有多种变形的写法,一定要注意这个问题,举一个会产生线程同步问题的例子:
public class Singleton { private static Singleton singleton = null; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }这种方案在没有并发的情况下不会出现任何问题,但若是出现了并发,就会在内存中产生多个实例。
原因是线程 A 在执行到 singleton = new Singleton() 这句话的时候,但是还没有完成实例的初始化操作,线程 B 恰巧执行到了 singleton == null 的判断,这时,线程 B 判断条件为真,也去执行 singleton 初始化的这句代码,就会造成线程 A 获得了一个对象,线程 B 也获得了一个对象。
解决线程不安全的方式有很多种,比如加一个 synchronized 关键字。
public class Singleton1 { private static Singleton1 singleton1 = null; private Singleton1() { } public static synchronized Singleton1 getInstance() { if (singleton1 == null) { singleton1 = new Singleton1(); } return singleton1; } }这种在代码块中使用 synchronized 关键字的方式名字叫做懒汉式单例,前面我们写的那个将军叫做饿汉式单例。
饿汉式和懒汉式的命名很有意思:
饿汉:类一旦加载,就把单例初始化完成,保证 getInstance 的时候,单例是已经存在的了。懒汉:懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,懒汉式本身是非线程安全的,为了实现线程安全有几种写法,上面那种加方法锁的方式有点笨重,我们还可以使用同步代码块,减少锁的颗粒大小。
public class Singleton2 { private static volatile Singleton2 singleton2; private Singleton2() { } public static synchronized Singleton2 getInstance() { // 第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入 if(singleton2 == null) { // 第一层锁,保证只有一个线程进入 synchronized (Singleton2.class) { // 第二层检查 if (singleton2 == null) { // volatile 关键字作用为禁止指令重排,保证返回 Singleton 对象一定在创建对象后 singleton2 = new Singleton2(); } } } return singleton2; } }关于 volatile 关键字多说两句,如果对象没有 volatile 关键字,这里会涉及到一个指令重排序问题, singleton2 = new Singleton2() 这句话实际上会涉及到以下三件事儿:
申请一块内存空间。
在这块空间里实例化对象。
singleton2 的引用指向这块空间地址。
对于以上步骤,指令重排序很有可能不是按上面 123 步骤依次执行的。比如,先执行 1 申请一块内存空间,然后执行 3 步骤, singleton2 的引用去指向刚刚申请的内存空间地址,那么,当它再去执行 2 步骤,判断 singleton2 时,由于 singleton2 已经指向了某一地址,它就不会再为 null 了,因此,也就不会实例化对象了。
而我们添加的关键字 volatile 就是为了解决这个问题,因为 volatile 可以禁止指令重排序。
不过还是建议大家使用饿汉式的单例模式,毕竟比较简单,出错的概率比较低。
还是刚才那个例子,如果一只军队中,偶然情况下出现了 3 个将军,士兵需要听从这 3 个将军的命令,我们用代码实现一下,这段代码稍微有点长:
public class General1 { // 定义最多能产生的将军数量 private static int maxNumOfGeneral1 = 3; // 定义一个列表,存放所有将军的名字 private static ArrayList<String> nameList = new ArrayList<> (); // 定义一个列表,容纳所有的将军实例 private static ArrayList<General1> general1ArrayList = new ArrayList<> (); // 定义当前将军序号 private static int countNumOfGeneral1 = 0; // 在静态代码块中产生所有的将军 static { for (int i = 0; i < maxNumOfGeneral1; i++) { general1ArrayList.add(new General1(String.valueOf(i))); } } private General1() { // 目的是不产生将军 } private General1(String name) { // 给将军加个名字,建立一个将军对象 nameList.add(name); } public static General1 getInstance() { // 随机产生一个将军,只要能发号施令就成 Random random = new Random(); countNumOfGeneral1 = random.nextInt(maxNumOfGeneral1); return general1ArrayList.get(countNumOfGeneral1); } public void command() { System.out.println("将军说:我是 " + nameList.get(countNumOfGeneral1) + " 号将军"); } }上面这段代码使用了两个 ArrayList 分别存储实例和实例变量。
如果考虑到线程安全的问题,可以使用 Vector 来代替,或者加锁等方式。
我们再创建一个士兵类,等将军发号施令:
public class Soldier1 { public static void main(String[] args) { for (int soldiers1 = 0; soldiers1 < 5; soldiers1++) { General1 general = General1.getInstance(); general.command(); } } }结果是这样的:
将军说:我是 0 号将军 将军说:我是 0 号将军 将军说:我是 1 号将军 将军说:我是 0 号将军 将军说:我是 2 号将军这种需要产生固定数量对象的模式就叫做有上限的多例模式,它是单例模式的一种扩展,采用有上限的多例模式,我们可以在设计时决定在内存中有多少个实例,方便系统进行扩展,修正单例可能存在的性能问题,提供系统的响应速度。例如读取文件,我们可以在系统启动时完成初始化工作,在内存中启动固定数量的 reader 实例,然后在需要读取文件时就可以快速响应。
您的扫码关注,是对小编坚持原创的最大鼓励:) 极客挖掘机 认证博客专家 Redis Java Spring 一个来自十八线城乡结合部破写代码的,平时喜欢读读书、写写代码,从2019年开始运营有自己的个人公众号:极客挖掘机,想交流的朋友可以来公众号找我聊天。