返回 登录
0

那么静,那么美,单例模式

单例模式

首先我们来讲一个故事。二次世界大战的时候,我国有一个著名的战役叫“长沙保卫战”,中国军队指挥官薛岳将军率领第9战区十余万将士,通过所谓的“焦土”战术4次瓦解日军的大规模进攻,给当时的国民党政府打了一针强心剂。这四次战役中最让人难忘的一幕是,面对单兵战斗力是中国军队5倍的日军,人数上虽然占据一定优势,但是只有第10军和第74军两只军队装备了现代化的军械,其余军队都是“汉阳造”的落后装备。薛将军命令第10军反复在湘北、赣北多处阵地来回穿插,面对东西方向出现的多路敌军,帮助装备落后的部队一起防守阵地,让敌人误以为是多支部队,其实薛岳将军只是调动了同一支部队,正是这一单一实例的对象(第10军)在各个战场均发挥出了显著的作用,为第二次长沙战役的全面获胜起了至关重要的作用。

回到我们的主题。考虑这样一个应用,读取配置文件的内容。很多应用项目,都有与应用相关的配置文件,这些配置文件很多是由项目开发人员自定义的,在里面定义一些应用重要的参数数据。当然,在实际的项目中,这种配置文件多数采用xml格式,也有采用properties格式的,我们这里假设创建了一个名为AppConfig的类,它专门用来读取配置文件内的信息。客户端通过new一个AppConfig的实例来得到一个操作配置文件内容的对象。如果在系统运行中,有很多地方都需要使用配置文件的内容,也就是说很多地方都需要创建AppConfig对象的实例。换句话说,在系统运行期间,系统中会存在很多个AppConfig的实例对象,这里读者有没有发现有什么问题存在?当然有问题了,试想一下,每一个AppConfig实例对象里面都封装着配置文件的内容,系统中有多个AppConfig实例对象,也就是说系统中会同时存在多份配置文件的内容,这样会严重浪费内存资源。如果配置文件内容越多,对于系统资源的浪费程度就越大。事实上,对于AppConfig这样的类,在运行期间只需要一个实例对象就足够了。
从专业化来说,单例模式是一种对象创建模式,它用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。Java里面实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机在通过自己的ClassLoad装载实现单例类的时候就会创建一个类的实例。在Java语言中,这样的行为能带来两大好处:
对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;

由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。

因此对于系统的关键组件和被频繁使用的对象,使用单例模式可以有效地改善系统的性能。单例模式的核心在于通过一个接口返回唯一的对象实例。首要的问题就是要把创建实例的权限收回来,让类自身来负责自己类的实例的创建工作,然后由这个类来提供外部可以访问这个类实例的方法,代码如清单1所示:

清单1.单例模式基本实现
图片描述
首先单例类必须要有一个private访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化;其次,instance成员变量和getInstance方法必须是static的。
上述代码唯一的不足是无法对instance实例做延时加载,例如单例的创建过程很慢,而由于instance成员变量是static定义的,因此在JVM加载单例类时,单例对象就会被建立,如果此时这个单例类在系统中还扮演其他角色,那么在任何使用这个单例类的地方都会初始化这个单例变量,而不管是否会被用到。代码如清单2所示:

清单2.单例模式实验
图片描述
上述代码运行后的输出如清单3所示:

清单3.清单2代码运行运行后输出
图片描述
可以看到,虽然此时并没有使用单例类,但它还是被创建出来,为了解决这类问题,需要引入延迟加载机制,代码如清单4所示:

清单4.延迟加载的单例模式代码
图片描述
上述代码运行后的输出如清单5所示:

清单5.清单4代码运行后输出
图片描述
清单4所示代码首先对于静态成员变量instance初始化赋值null,确保系统启动时没有额外的负载;其次,在getInstance()工厂方法中,判断当前单例是否已经存在,若存在则返回,不存在则再建立单例。这里尤其要注意的是,getInstance()方法必须是同步的,否则在多线程环境下,当线程1正新建单例时,完成赋值操作前,线程2可能判断instance为null,故线程2也将启动新建单例的程序,而导致多个实例被创建,故同步关键字是必须的。由于引入了同步关键字,导致多线程环境下耗时明显增加。两者测试代码如清单6所示:

清单6.非同步的单例模式代码
图片描述
清单6所示代码运行的输出如清单7所示:

清单7.清单6代码运行后输出
图片描述
清单8.完整的延迟加载方式代码
图片描述
清单9.清单8代码运行后输出
图片描述
为了解决同步关键字降低系统性能的缺陷,做了一定改进,代码如清单10所示:

清单10.解决同步关键字低效率
图片描述
清单10的单例模式使用内部类来维护单例的实例,当StaticSingleton被加载时,其内部类并不会被初始化,故可以确保当StaticSingleton类被载入JVM时,不会初始化单例类,而当getInstance()方法调用时,才会加载SingletonHolder,从而初始化instance。同时,由于实例的建立是在类加载时完成,故天生对多线程友好,getInstance()方法也无需使用同步关键字。
单例模式的本质
单例模式是为了控制在运行期间,某些类的实例数目只能有一个。如果你想要控制多个,可以利用Map来帮助缓存多个实例,代码如清单11所示:

清单11.控制3个实例
图片描述
清单11所示代码的输出如清单12所示:

清单12.清单11运行输出
图片描述
第一个实例和第四个相同,第二个与第五个相同,第三个与第六个相同。也就是说一共只创建了三个实例。
相关图书
图片描述
《大话Java性能优化》
轻松道破软件性能调优方法论和具体实现路径
全面细致,一本书搞定性能优化
周明耀 著
2016年4月出版
◎ 系统介绍系统调优的解决思路和技术实现
◎ 结合大家最为熟知的12306、电商等案例
◎ 架构、设计、开发、算法等多层次多角度思路和策略
◎ 涉及内存、IO等各种问题,提供丰富的经验参考
◎ 语言通俗易懂,引人入胜
图片描述

评论