Skip to content

Latest commit

 

History

History
356 lines (266 loc) · 12.3 KB

单例模式.md

File metadata and controls

356 lines (266 loc) · 12.3 KB

单例模式

延迟加载、锁机制、静态类等单例模式实现方法

标签: Java

原文链接

周 明耀

发布: 2015-05-04


单例模式

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

回到我们的主题。考虑这样一个应用,读取配置文件的内容。很多应用项目,都有与应用相关的配置文件,这些配置文件很多是由项目开发人员自定义的,在里面定义一些应用重要的参数数据。当然,在实际的项目中,这种配置文件多数采用 xml 格式,也有采用 properties 格式的,我们这里假设创建了一个名为 AppConfig 的类,它专门用来读取配置文件内的信息。客户端通过 new 一个 AppConfig 的实例来得到一个操作配置文件内容的对象。如果在系统运行中,有很多地方都需要使用配置文件的内容,也就是说很多地方都需要创建 AppConfig 对象的实例。换句话说,在系统运行期间,系统中会存在很多个 AppConfig 的实例对象,这里读者有没有发现有什么问题存在?当然有问题了,试想一下,每一个 AppConfig 实例对象里面都封装着配置文件的内容,系统中有多个 AppConfig 实例对象,也就是说系统中会同时存在多份配置文件的内容,这样会严重浪费内存资源。如果配置文件内容越多,对于系统资源的浪费程度就越大。事实上,对于 AppConfig 这样的类,在运行期间只需要一个实例对象就足够了。

单例模式的实现

从专业化来说,单例模式是一种对象创建模式,它用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。Java 里面实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机在通过自己的 ClassLoad 装载实现单例类的时候就会创建一个类的实例。在 Java 语言中,这样的行为能带来两大好处:

  1. 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
  2. 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

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

清单 1. 单例模式基本实现
public class Singleton {
private Singleton(){
System.out.println("Singleton is create");
}
private static Singleton instance = new Singleton();
public static Singleton getInsatnce(){
return instance;
}
}

Show moreShow more icon

首先单例类必须要有一个 private 访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化,;其次,instance 成员变量和 getInstance 方法必须是 static 的。

上述代码唯一的不足是无法对 instance 实例做延时加载,例如单例的创建过程很慢,而由于 instance 成员变量是 static 定义的,因此在 JVM 加载单例类时,单例对象就会被建立,如果此时这个单例类在系统中还扮演其他角色,那么在任何使用这个单例类的地方都会初始化这个单例变量,而不管是否会被用到。代码如清单 2 所示:

清单 2. 单例模式实验
public class Singleton {
private Singleton(){
System.out.println("Singleton is create");
}
private static Singleton instance = new Singleton();
public static Singleton getInsatnce(){
return instance;
}
public static void createString(){
System.out.println("createString in Singleton");
}

public static void main(String[] args){
Singleton.createString();
}
}

Show moreShow more icon

上述代码运行后的输出如清单 3 所示:

清单 3. 清单 2 代码运行运行后输出
Singleton is create
createString in Singleton

Show moreShow more icon

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

清单 4. 延迟加载的单例模式代码
public class LazySingleton {
private LazySingleton(){
System.out.println("LazySingleton is create");
}
private static LazySingleton instance = null;
public static synchronized LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
public static void createString(){
System.out.println("create String");
}

public static void main(String[] args){
LazySingleton.createString();
}
}

Show moreShow more icon

上述代码运行后的输出如清单 5 所示:

清单 5. 清单 4 代码运行后输出
create String

Show moreShow more icon

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

清单 6. 非同步的单例模式代码
public class Singleton implements Runnable{
private Singleton(){
System.out.println("Singleton is create");
}
private static Singleton instance = new Singleton();
public static Singleton getInsatnce(){
return instance;
}
public static void createString(){
System.out.println("createString in Singleton");
}

@Override
public void run() {
// TODO Auto-generated method stub
long beginTime = System.currentTimeMillis();
for(int i=0;i<10000;i++){
Singleton.getInsatnce();
}
System.out.println(System.currentTimeMillis() - beginTime);
}

public static void main(String[] args){
//Singleton.createString();
for(int i=0;i<5;i++){
new Thread(new Singleton()).start();
}
}

}

Show moreShow more icon

清单 6 所示代码运行的输出如清单 7 所示:

清单 7. 清单 6 代码运行后输出
Singleton is create
Singleton is create
Singleton is create
0
Singleton is create
Singleton is create
0
0
Singleton is create
0
0

Show moreShow more icon

清单 8. 完整的延迟加载方式代码
public class LazySingleton implements Runnable{
private LazySingleton(){
System.out.println("LazySingleton is create");
}
private static LazySingleton instance = null;
public static synchronized LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
public static void createString(){
System.out.println("create String");
}

@Override
public void run() {
// TODO Auto-generated method stub
long beginTime = System.currentTimeMillis();
for(int i=0;i<1000000;i++){
LazySingleton.getInstance();
}
System.out.println(System.currentTimeMillis() - beginTime);
}

public static void main(String[] args){
//LazySingleton.createString();
for(int i=0;i<5;i++){
new Thread(new LazySingleton()).start();
}
}

}

Show moreShow more icon

清单 9. 清单 8 代码运行后输出
LazySingleton is create
LazySingleton is create
LazySingleton is create
LazySingleton is create
LazySingleton is create
LazySingleton is create
1139
1202
1234
1218
1234

Show moreShow more icon

为了解决同步关键字降低系统性能的缺陷,做了一定改进,代码如清单 10 所示:

清单 10. 解决同步关键字低效率
public class StaticSingleton {
private StaticSingleton(){
System.out.println("StaticSingleton is create");
}
private static class SingletonHolder{
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance(){
return SingletonHolder.instance;
}
}

Show moreShow more icon

清单 10 的单例模式使用内部类来维护单例的实例,当 StaticSingleton 被加载时,其内部类并不会被初始化,故可以确保当 StaticSingleton 类被载入 JVM 时,不会初始化单例类,而当 getInstance() 方法调用时,才会加载 SingletonHolder,从而初始化 instance。同时,由于实例的建立是时在类加载时完成,故天生对多线程友好,getInstance() 方法也无需使用同步关键字。

单例模式的本质

单例模式是为了控制在运行期间,某些类的实例数目只能有一个。如果你想要控制多个,可以利用 Map 来帮助缓存多个实例,代码如清单 11 所示:

清单 11. 控制 3 个实例
import java.util.HashMap;
import java.util.Map;

/**
* 扩展单例模式,控制实际产生实例数目为 3 个
* @author zhoumingyao
*
*/
public class ThreeSingleton {
private final static String DEFAULT_PREKEY = "cache";//为后面使用的 key 定义一个前缀
private static Map<String,ThreeSingleton>
map = new HashMap<String,ThreeSingleton>();//定义缓存实例的容器
private static int number = 1;//定义初始化实例数目为 1
private final static int NUM_MAX = 3;

private ThreeSingleton(){

}

public static synchronized ThreeSingleton getInstance(){
//通过缓存理念及方式控制数量
String key = DEFAULT_PREKEY + number;
ThreeSingleton threeSingleton = map.get(key);
if(threeSingleton==null){
threeSingleton = new ThreeSingleton();
map.put(key, threeSingleton);
}
number++;//实例数目加 1
if(number>NUM_MAX){
number=1;
}
return threeSingleton;
}

public static void main(String args[]){
ThreeSingleton t1 = getInstance();
ThreeSingleton t2 = getInstance();
ThreeSingleton t3 = getInstance();
ThreeSingleton t4 = getInstance();
ThreeSingleton t5 = getInstance();
ThreeSingleton t6 = getInstance();
System.out.println(t1.toString());
System.out.println(t2.toString());
System.out.println(t3.toString());
System.out.println(t4.toString());
System.out.println(t5.toString());
System.out.println(t6.toString());
}
}

Show moreShow more icon

清单 11 所示代码的输出如清单 12 所示:

清单 12. 清单 11 运行输出
ThreeSingleton@61de33
ThreeSingleton@14318bb
ThreeSingleton@ca0b6
ThreeSingleton@61de33
ThreeSingleton@14318bb
ThreeSingleton@ca0b6

Show moreShow more icon

第一个实例和第四个相同,第二个与第五个相同,第三个与第六个相同。也就是说一共只创建了三个实例。

结束语

单例模式是用来实现在整个程序中只有一个实例的设计模式方法。本文通过从最基础的单例模式开始讲解,逐渐进入到延迟加载、锁机制、内部、静态类等单例模式实现方法,让读者可以学习单例模式的具体应用。设计模式的核心理念是活学活用,所以不会有什么规则是固定不变的规则,需要读者在实际应用过程中不断尝试、不断创新,力求代码间接明了、易于扩展。

相关主题