设计模式第一部分: 单例模式

编程入门 行业动态 更新时间:2024-10-12 18:15:18

设计<a href=https://www.elefans.com/category/jswz/34/1771241.html style=模式第一部分: 单例模式"/>

设计模式第一部分: 单例模式

原创:/

本文首先讲解了单例模式的基本概念,通过讲故事的方式带领读者进入这篇文章,接着通过文字加代码的方式从最基本的单例模式讲起,逐渐进入到延迟加载、锁机制、静态类等单例模式实现方法。通过本文读者基本可以掌握单例模式的应用。

单例模式

首先我们来讲一个故事。二次世界大战的时候,我国有一个著名的战役叫“长沙保卫战”,中国军队指挥官薛岳将军率领第 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;}
}

首先单例类必须要有一个 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();}
}

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

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

可以看到,虽然此时并没有使用单例类,但它还是被创建出来,为了解决这类问题,需要引入延迟加载机制,代码如清单 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();}
}

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

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

清单 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");}@Overridepublic void run() {// TODO Auto-generated method stublong 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();}}}

清单 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
清单 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");}@Overridepublic void run() {// TODO Auto-generated method stublong 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();}}}
清单 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

为了解决同步关键字降低系统性能的缺陷,做了一定改进,代码如清单 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;}
}

清单 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;//定义初始化实例数目为 1private 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++;//实例数目加 1if(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());}
}

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

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

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

结束语

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


更多推荐

设计模式第一部分: 单例模式

本文发布于:2024-03-23 20:28:36,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1742479.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:模式   第一部分

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!