文档章节

单例模式大汇总

乒乓狂魔
 乒乓狂魔
发布于 2015/02/07 10:38
字数 1389
阅读 253
收藏 3
看了多方资料,整理下单例设计模式,有不少值得相互探究的地方,你就会发现就这一个小小的单例模式竟然映射出N多知识。我在这里把问题综述出来,一起相互探讨。

单例涉及到的相关文章如下:
                反射、枚举与单例
                序列化与单例
                类加载器与单例

本文则主要是讲多线程与单例。
单例模式首先分为懒汉式和饿汉式。所谓饿汉式即一开始就创建出单例对象,懒汉式则为当需要使用的时候才会去创建出单例对象。

先看下饿汉式:
public final class Singleton {

	private static final Singleton instance=new Singleton();
	
	private Singleton(){}
	
	public static Singleton getInstance(){
		return instance;
	}
}

1 私有化构造器,使得别人无法再创建新对象。
问题1:即使私有化构造器,别人仍然可以通过反射机制来创建新对象,要是这样的话,下面的很多单例方法都不再是单例。然而枚举除外。

2 这种饿汉式的方式在类加载器加载Singleton的时候就会去初始化创建一个Singleton实例,类加载器加载Singleton时线程安全的,所以这种方式不存在线程安全问题。

懒汉式:有时候为了在使用的时候才去创建单例对象,就要采用懒汉式

public final class Singleton {

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

这种方式即在需要的时候才会去创建单例对象。很明显,大家都知道这种方式引入了线程安全问题,所以要对getInstance方法加上锁,如下:
public final class Singleton {

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

这样的话,每个线程要执行getInstance方法时,synchronized对他们进行了同步,保证并发情况下只有一个线程在执行getInstance方法。这种做法的的确解决了线程安全问题,但是却造成了很大的性能开销。因为instance只需要在第一次创建时进行同步,创建后每次获取时不需要再进行同步,所以我们要进一步改进:
public final class Singleton {

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

这种方式即缩小了同步的范围,保证了在单例对象创建出来后,每次获取时不需要再进行同步,但是又造成了一个问题,即不能保证instance=new Singleton()只被执行一次,所以又要改进,需要在同步的代码中再次检查是否已经创建,如下:
public final class Singleton {

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

这就是所谓的双重检查机制。看似已经完美,实则不然。instance=new Singleton()实际上分为三个过程:
1 分配内存
2 对Singleton的一些初始化工作包括构造函数的执行
3 对instance变量赋值内存地址
然而对于第2步和第3步,不同的编译器由于执行了优化导致他们的执行顺序并不一致,即发生了重排序,对于重排序参见这篇infoq上的文章http://www.infoq.com/cn/articles/java-memory-model-2
也就是线程1当执行到第2步的时候,instance就已经有值了,此时线程2执行getInstance方法的最外层的if(instance==null)判断就会直接返回。然而该对象还没有真正的完成初始化,还不能正常使用。此时线程2如果去使用该对象,就会出问题了。

为了解决这个问题,就是不允许第2步和第3步进行重排序,使用volatile来解决,如下:

public final class Singleton {

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

只需要在Singleton instance变量上加上volatile修饰,就可以禁止重排序。我们知道synchronized 即保证可见性又保证了互斥性,而volatile则仅仅是保持了可见性,而这里volatile又起到禁止重排序的功能(我也不懂,留给大神们去研究)。

另一种解决方案是,基于类初始化的解决方案:

public final class Singleton {

	private static class SingletonHolder{
		public static Singleton instance=new Singleton();
	}
	
	private Singleton(){}
	
	public static  Singleton getInstance(){
		return SingletonHolder.instance;
	}
}

这也是一种常见的懒汉式单例,接下来我们就要分析分析它是如何解决多线程问题的。
当多个线程执行SingletonHolder.instance时,会首先进行类的初始化,即多个线程可能同时去初始化同一个类,这方面对于jvm来说是进行了细致的同步,每个类都有一个初始化锁,来确保只能有一个线程来初始化类。当线程A获取了SingletonHolder类的初始化锁,线程B则需要等待,线程A就要去执行SingletonHolder的静态变量表达式、静态代码块等初始化工作,然后就能确保Singleton instance=new Singleton()只被一个线程来执行。
总的来说,此种方法是依靠jvm对类和接口的同步来实现单例线程安全的。具体jvm对于类和接口初始化的同步过程可以见这篇文章http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization

若想转载请注明出处
作者:乒乓狂魔

© 著作权归作者所有

共有 人打赏支持
乒乓狂魔
粉丝 994
博文 105
码字总数 271356
作品 0
长宁
程序员
加载中

评论(1)

rock912
rock912
不错
编程中的那些套路——关于策略模式

该文章属于《编程中的那些经典套路——设计模式汇总》系列,并且以下内容基于语言PHP 今天讲讲策略模式,策略模式 和工厂模式十分相像(或者说在代码逻辑层面,他们是一样的)。 但策略模式与...

gzchen
08/27
0
0
编程中的那些套路——关于工厂模式

该文章属于《编程中的那些经典套路——设计模式汇总》系列,并且以下内容基于语言PHP 前面我们写了简单工厂模式,《编程中的那些套路——关于简单工厂模式》,但简单工厂模式有一些不足(违反...

gzchen
08/27
0
0
编程中的那些套路——关于单例模式

该文章属于《编程中的那些经典套路——设计模式汇总》系列,并且以下内容基于语言PHP 在设计模式中,单例模式和工厂模式)可以说是使用的最普遍的设计模式了,所以掌握此种模式尤为重要。 单...

gzchen
08/27
0
0
编程中的那些经典套路——设计模式汇总

在正式阅读前,我先谈谈我们该用什么姿势和心态学习设计模式: 如果你还没有过多的编程经验(泛指半年以下),我建议你把它当做小说来看,能看懂多少是多少,因为半年以下经验的程序员用到设...

gzchen
08/27
0
0
编程中的那些套路——关于简单工厂模式

该文章属于《编程中的那些经典套路——设计模式汇总》系列,并且以下内容基于语言PHP 面向对象五大原则中有一点非常重要的原则:单一职责原则。 简单工厂模式就是遵循了这一原则,它让不同职...

gzchen
08/27
0
0

没有更多内容

加载失败,请刷新页面

加载更多

科学利用谷歌云平台

当今既是大数据时代,也是云计算时代。云平台构建已经成了诸多大数据平台建设第一步。于是各家互联网巨头们纷纷都开启了云平台服务。国内的以阿里云、腾讯云、百度云、华为云为首,国外的有A...

胖胖雕
14分钟前
0
0
公众号关联小程序发送关联通知

公众号关联小程序发送关联通知,对于推广小程序有着很大的帮助。所以问题来了,怎么做到在公众号关联小程序发送关联通知呢? 一:开发中遇到的问题 之前在开发过程中发现,公众号已经关联小程...

Code辉
17分钟前
0
0
并发编程基础之JMM学习摘要

一、JMM定义 Java内存模型即Java Memory Model(JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见(内存可见性),从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程...

狠一点
22分钟前
0
0
mysql 开启日志记录并且解决日志时间错误问题

一、开启二进制日志 查看二进制日志是否开启 mysql> show variables like 'log_bin'; 编辑mysql配置文件 vi /etc/mysql/mysql.conf.d/mysqld.cnf 注意log-bin和log_bin这个坑 server-id = 1 ......

Marhal
24分钟前
0
0
Kubernetes上的负载均衡详解

如果您的应用程序是面向大量用户、会吸引大量流量,那么一个不变的目标一定是在高效满足用户需求的同时、不让用户感知到任何类似于“服务器繁忙!”的情况。这一诉求的典型解决方案是横向扩展...

RancherLabs
24分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部