关闭

设计模式学习之路 - 单例模式 - only you

标签: 设计模式单例模式
182人阅读 评论(0) 收藏 举报
分类:

今天我们了解一下单例模式,这个模式似乎是笔试最多的模式之一了(面试几乎必问, 似乎面试官特别感兴趣)

单例,从字面意思看, 就是单独的实例, 表示这个实例是唯一的。

那么很多人就会问了,为什么需要这种只有一个实例的类。

其实,在开发中, 很多对象我们都只需要一个,比如:线程池、缓存等等.

实际上这些对象也有且只能有一个,多个实例的话,反而会有问题,或为程序异常,或为资源不足。


我们先贴一下单例模式的定义: 确保一个类只有一个实例, 并提供一个全局访问点


单例模式的代码其实很简单,按照正常的思路会这样做。

package com.chris.single;

public class Singleton {
	private static Singleton uniqueInstance;
	
	private Singleton(){
		
	}
	
	public static Singleton getInstance(){
		if(uniqueInstance == null){
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
}

定义一个静态变量, 当变量存在就返回,不存在就直接创建一个,这样就只有一个实例了。


如果一般的情况,这段代码好像没有什么问题,能确保只有一个实例。

但是, 这时候又要扯到另一个面试必问的问题了, 多线程!!

当有多个线程去执行获取实例的方法时, 可能会同时会判断到uniqueInstance == null 这个条件,这时候就会同时创建实例,这个类就有两个实例了。

有两个实例就不是only you了, 就不是唯一了,所以我们需要对多线程的情况稍微处理一下。


一到同步的问题,大多数开发人员脑海中立刻回浮现出一个单词 : synchronized!!

的确, 只要用synchronized对方法进行修饰, 多线程的灾难几乎就可以轻易的解决。

package com.chris.single;

public class Singleton {
	private static Singleton uniqueInstance;
	
	private Singleton(){
		
	}
	
	public static synchronized Singleton getInstance(){
		if(uniqueInstance == null){
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
}

synchronized关键字,可以保证一个线程进入这个方法之后,对其加一个线程锁, 其他线程在这个线程离开这个方法之前,

都必须等待,不会有两个线程同时进入这个方法。


这个方法好像是操作起来最快的方法了, 加一个关键字进行修饰就可以解决。

但是,在很多书上都会提及, synchronized关键字, 是一个十分重量级的东西,会很大的影响性能,无论是修饰方法或者同步一个代码块。

所以,如果你的应用程序能够接受这个造成的额外的性能负担, 那么上述代码就是你想要的单例模式, 这个实现方式叫懒加载(lazy-init)!


或者, 你的应用程序在创建和运行时的负担不是很重, 这时候我们也可以使用更简便的方式, 急加载(eagerly-init)!

package com.chris.single;

public class Singleton {
	private static Singleton uniqueInstance = new Singleton();
	
	private Singleton(){
		
	}
	
	public static Singleton getInstance(){
		return uniqueInstance;
	}
}

这种方式会在程序编译后立即加载这个类并创建唯一的实例,而不是在运行时创建, 这样就可以保证只有一个实例。

但是这个类一直都没有被用到的话,这就会浪费空间了,这也是这个方式的一个缺点。


然后如果想综合考虑的话, 这里就有另一种解决办法, 双重校验锁(double-cheked locking)!

package com.chris.single;

public class Singleton {
	private volatile static Singleton uniqueInstance;
	
	private Singleton(){
		
	}
	
	public static Singleton getInstance(){
		if(uniqueInstance == null){
			synchronized (Singleton.class) {
				if(uniqueInstance == null){
					uniqueInstance = new Singleton();
				}
			}
		}
		return uniqueInstance;
	}
}
为什么叫双重, 因为他有两个关于同步的关键字, 一个是synchronized, 另一个是volatile.

这个方式会首先检查实例是否被创建,如果未创建,这时候才进行同步,所以,同步只会有一次,相比懒加载方式,这个会大大的减少时间耗费,提高性能。

这个方式好像看起来十分的完美了,但是任何介绍这个双重校验锁方式的读物,都会强调一个事情。

在1.4以及更早的版本中,许多JVM对volatile这个关键字的实现会导致双重校验的失效,所以如果不能用java5以后的版本,就不要用这个方式了(用4以前的几乎没有了吧!)


然后我们聊聊volatile,为什么要用这个关键字呢, 因为虽然synchronized能保证进入创建实例的代码是同步的,但是new Singleton() 这个操作缺却不是原子的。

在创建实例的时候,我们的JVM一般会做三件事:

1. 给uniqueInstance分配内存;

2.调用类的构造方法初始化成员变量;

3.将uniqueInstance对象指向分配的内存空间。

当操作3完成之后,其实这个对象就已经不为null了。

而JVM中的JIT(即时编译器)存在指令重排序的优化,所以,步骤2和步骤3的顺序是不能保证的。

最终可能为1-2-3, 也可能为1-3-2,而如果是后者的话,先完成了内存的分配,而这时候另一个线程进来了,判断实例不为null, 直接返回,

但是这个实例却没有经过第2步初始化, 程序就理所当然的报错了。

volatile这个关键字能保证修饰的参数对所有的线程都是可见的, 并且禁止指令重排序优化

对所有的线程可见,是表示线程中不会存在该实例的副本,每次需要拿的话都会去主内存中拿。

而禁止指令重排序优化,就如上述所说,能保证他的操作的执行顺序在JIT进行优化时不会被打乱。


在开发中,自己写单例好像也比较少,一般都是用的第三方的,所以只能了解一下大概的概念。

如果在文中有什么错误的地方,还望指正,和大家共勉。。。






0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:14983次
    • 积分:508
    • 等级:
    • 排名:千里之外
    • 原创:35篇
    • 转载:10篇
    • 译文:0篇
    • 评论:6条
    文章分类
    最新评论