设计模式 —– 单件模式
个人博客,想要搭建个人博客的可以进来看看: http://www.ioqian.top/
单件模式,确保一个类只有一个实例额,并提供一个安全的全局访问点
所有设计模式中最简单的一种,但是在软件开发中使用的很频繁
背景 , 在我们的程序设计过程中,经常会用到一个类的实现在整个程序中只有一个初始化一个实例,为什么会出现这种需求哪?
1.多个实例可能会出现在不同的线程中同时操作同一个设备,比如同时读写同一个配置文件,同时操作打印设备,都会产生数据的不确定性
2.实例化一个类的代价很大,没必要在每个类中都实例化一个新的,比如在javaJDK中Calendar的实现本身就只能通过单利模式实现
1.不同类的初始化耗时差别很大。测试实例化10000个Date类型和100000个String类型和1个Calendar的耗时来进行分析,为什么只有1个Calendar,因为java自身的实现Calendar自身就使用了单例模式,没办法去初始化100000个。
public static void main(String[] args) {
//初始化100000个Date类
long start = System.currentTimeMillis();
List<Date> list = new ArrayList<>(100000);
for(int i = 0 ; i < 100000 ; i++){
list.add(new Date());
}
long end = System.currentTimeMillis();
System.out.println("初始化100000个Date 耗时"+(end - start)+"ms");
//初始化100000个String类
start = System.currentTimeMillis();
List<String> lists = new ArrayList<>(100000);
for(int i = 0 ; i < 100000 ; i++){
lists.add(new String(""));
}
end = System.currentTimeMillis();
System.out.println("初始化100000个String 耗时"+(end - start)+"ms");
//初始化一个Calendar类
start = System.currentTimeMillis();
Calendar cc = Calendar.getInstance();
end = System.currentTimeMillis();
System.out.println("初始化1个Calendar耗时"+(end - start)+"ms");
}
//结果
初始化100000个Date 耗时16ms
初始化100000个String 耗时3ms
初始化Calendar耗时26ms
Process finished with exit code 0
分析,对应Date和String虽然我们看起来十万个类的差别才9ms,看起来微不足道,但是换一种说话,每初始化一个Date类的耗时是初始化一个String类的4倍,这个4倍的概念就很大了吧,在java中肯定存在实例化特别耗时的一个类,比如说最后的Calendar,初始化一次就耗时26ms是初始化一次String的八十多万倍。(简直是天文数字,有没有)。所以针对这些类的实现必须使用单例模式(也就是单件模式)
2.单例模式
单例模式有以下3个特点:
- 1.只能有一个实例。
- 2.必须自行创建这个实例。
- 3.必须给其他对象提供这一实例。
初级版本的单例模式
public class PrimaryInstance {
//在不需要的时候不需要初始化
private static PrimaryInstance instance = null;
//构造方法设为private,外部无法初始化
private PrimaryInstance(){}
//得到PrimaryInstance唯一的方法
public static PrimaryInstance getInstance(){
if(instance == null){ //语句a
instance = new PrimaryInstance();
}
return instance;
}
}
看起来很完美,实现了单利模式的三个特点,但是当在多线程中使用时,出现了问题,得到了2个实例(我是jvm,两个线程同时进入getInstance()方法,当线程1刚进入语句a返回true时,线程1时间片到了,线程2开始执行,线程2判断语句a也是true,线程2实例化一个实例然后返回实例1,又到了线程1的时间,线程1上次已经判断语句为true然后也实例化一个实例,悲剧了…),违背了只能有一个实例的特点,既然是多线程引起的那简单了,直接加上synchronized关键字
中级版本的单例模式
public class SecondInstance {
//在不需要的时候不需要初始化
private static SecondInstance instance = null;
//构造方法设为private,外部无法初始化
private SecondInstance(){}
//得到PrimaryInstance唯一的方法
public static synchronized SecondInstance getInstance(){
if(instance == null){
instance = new SecondInstance();
}
return instance;
}
}
这个中级版本的代码确实解决了多线程错误的问题,但是这样付出的代价太大了,每次获取实例都需要同步,太耗费时间了,我们只需要在第一次执行getInstance()有同步操作就可以了。那我们应该怎么办?
- 1.如果getInstance()的性能对程序不是很关键,就不修改了,但是如果getInstance()在多线程中频繁的使用,哪就是一个噩梦了
- 2.在类初始化的过程中就进行实例的初始化
public class NowInstance {
//在静态声明中进行初始化
private static NowInstance instance = new NowInstance();
private NowInstance(){}
public static NowInstance getInstance(){
return instance;
}
}
- 3.最好的选择,用双重检测加锁机制,把synchronized操作放在方法的代码块中,就是最终的单例模式版本
终极版本的单利模式
public class AdvancedInstance {
private static AdvancedInstance instance = null;
private AdvancedInstance(){}
public static AdvancedInstance getInstance(){
if(instance == null){
//同步操作放在这里保证了只有第一次实例化对象才进行同步操作
synchronized (AdvancedInstance.class){
//进入同步代码区后,再检查一次,可以解决中级代码中遇到的问题
if(instance == null){
instance = new AdvancedInstance();
}
}
}
return instance;
}
}
注意,当存在多个类加载时,可能会出现多个实例,所以当同时使用了多个类加载器,又使用了单例模式请小心;解决办法就是自行指定类加载器,并指定为同一个类加载器