KandQ:单例模式的七种写法及其相关问题解析
设计模式中的单例模式可以有7种写法,这7种写法有各自的优点和缺点:
代码示例(java)及其分析如下:
一、懒汉式
public class Singleton
{
private static Singleton singleton;
private Singleton()
{
}
public static Singleton getInstance()
{
if (singleton == null)
singleton = new Singleton();
return singleton;
}
}
优点:
不是马上就初始化的,当需要使用的时候才进行初始化(即是lazy loading)
缺点:
在并发情况下是线程不安全的
二、懒汉式线程安全版
public class Singleton
{
private static Singleton singleton;
private Singleton()
{
}
public synchronized static Singleton getInstance()
{
if (singleton == null)
singleton = new Singleton();
return singleton;
}
}
优点:
不是类加载之后就进行初始化的,当需要使用的时候才进行初始化(即是lazy loading),且为线程安全的
缺点:
效率低,加了synchronized进行同步之后,效率上有所降低
三、饿汉式
public class Singleton
{
private static Singleton singleton = new Singleton();
private Singleton()
{
}
public static Singleton getInstance()
{
return singleton;
}
}
这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。其一个明显的好处就是是线程安全的
四、饿汉式的变种写法
public class Singleton
{
private static Singleton singleton;
static
{
singleton = new Singleton();
}
private Singleton()
{
}
public static Singleton getInstance()
{
return singleton;
}
}
其会在类加载的时候就进行加载。和上面的饿汉式的写法优缺点相同
五、静态内部类方式
public class Singleton
{
private Singleton()
{
}
private static class SingletonHolder
{
private static final Singleton singleton = new Singleton();
}
public static Singleton getInstance()
{
return SingletonHolder.singleton;
}
}
这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别): 第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。
六、采用枚举方式
public enum Singletons
{
INSTANCE;
// 此处表示单例对象里面的各种方法
public void Method()
{
}
}
Effective Java作者Josh Bloch提倡使用枚举的方式去实现单例模式。因为它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,同时写法简单。对于枚举方式创建单例,为何可以避免多线程的同步以及防止反序列化重新创建新的对象这个原因,详见相关博文:K:枚举的线程安全性及其序列化问题
七、双重校验锁
public class Singleton
{
private volatile static Singleton singleton;
private Singleton()
{
}
public static Singleton getInstance()
{
if (singleton == null)
{
synchronized (Singleton.class)
{
if (singleton == null)
{
singleton = new Singleton();
}
}
}
return singleton;
}
}
对于双重校验锁,其是对懒汉式线程安全版的改进,其目的在于减少同步所用的开销。对singleton进行两次判null检查,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例。
对singleton变量使用volatile关键字的原因是,instance = new Singleton()这句,并非是一个原子操作,事实上在 JVM中这句话大概做了下面3件事情:
- 给 instance 分配内存
- 调用 Singleton的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步instance就为非null了)
但是在 JVM的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是1-2-3也可能是1-3-2。如果是后者,则在3执行完毕、2未执行之前,被线程二抢占了,这时instance已经是非null了(但却没有初始化),所以线程二会直接返回instance,然后使用,然后顺理成章地jvm就会报错。
有些人认为使用volatile的原因是可见性,也就是可以保证线程在本地不会存有instance的副本,每次都是去主内存中读取。但其实是不对的。使用volatile的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在volatile变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完1-2-3之后或者1-3-2之后,不存在执行到1-3然后取到值的情况。从「先行发生原则」(即happen-before)的角度理解的话,就是对于一个volatile变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
对于第一种和第二种写法,实际上其可以归类为懒汉式这一种写法,对于第三种和第四种,其也可以归为饿汉式这一种写法。为此,一般单例都是五种写法。懒汉,饿汉,双重校验锁,枚举和静态内部类
对于单例模式,其有两个问题需要注意:
如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
对第一个问题修复的办法是:
private static Class getClass(String classname)throws ClassNotFoundException
{
// 获取当前执行线程的上下文类加载器
ClassLoader classLoader = Thread.currentThread()
.getContextClassLoader();
if (classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
对第二个问题修复的办法是:
class Singletones implements java.io.Serializable
{
private static Singletones INSTANCE = new Singletones();
private Singletones()
{
}
public static Singletones getInstance()
{
return INSTANCE;
}
/*
* 我们反序列化后获得的并不是原来的对象,而是经过重构的新的对象实例。
* ObjectInputStream对象在反序列化的时候,会在从I/O流中读取对象时
* ,调用readResolve()方法。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中重构的对象。
*/
private Object readResolve()
{
return Singletones.getInstance();
}
}
原因详见博文:K:java中序列化的两种方式—Serializable或Externalizable
KandQ:单例模式的七种写法及其相关问题解析的更多相关文章
- Java设计模式之单例模式(七种写法)
Java设计模式之单例模式(七种写法) 第一种,懒汉式,lazy初始化,线程不安全,多线程中无法工作: public class Singleton { private static Singleto ...
- Java 单例模式的七种写法
Java 单例模式的七种写法 第一种(懒汉,线程不安全) public class Singleton { private static Singleton instance; private Sin ...
- Android设计模式之单例模式的七种写法
一 单例模式介绍及它的使用场景 单例模式是应用最广的模式,也是我最先知道的一种设计模式.在深入了解单例模式之前.每当遇到如:getInstance()这样的创建实例的代码时,我都会把它当做一种单例模式 ...
- Java:单例模式的七种写法
第一种(懒汉,线程不安全): 1 public class Singleton { 2 private static Singleton instance; 3 private Singleton ( ...
- Java:单例模式的七种写法(转载)
第一种(懒汉,线程不安全): package Singleton; /** * @echo 2013-10-10 懒汉 线程不安全 */ public class Singleton1 { priva ...
- Java:单例模式的七种写法[转]
第一种(懒汉,线程不安全): 1 public class Singleton { 2 private static Singleton instance; 3 privat ...
- 【JAVA学习】单例模式的七种写法
尊重版权:http://cantellow.iteye.com/blog/838473 第一种(懒汉.线程不安全): Java代码 public class Singleton { private ...
- Java:单例模式的七种写法<转>
第一种(懒汉,线程不安全): 1 public class Singleton { 2 private static Singleton instance; 3 privat ...
- 温故而知新(java实现)单例模式的七种写法
第一种(懒汉,线程不安全): Java代码 public class Singleton { private static Singleton instance; private Singleton ...
随机推荐
- TensorFlow 处理图片
目标:介绍如何对图像数据进行预处理使训练得到的神经网络模型尽可能小地被无关因素所影响.但与此同时,复杂的预处理过程可能导致训练效率的下降.为了减少预处理对于训练速度的影响,TensorFlow 提供了 ...
- 【NOI2005】维护数列
https://daniu.luogu.org/problem/show?pid=2042 一道伸展树维护数列的很悲伤的题目,共要维护两个标记和两个数列信息,为了维护MAX-SUM还要维护从左端开始的 ...
- linux系统编程:进程间通信-fifo
进程间通信-fifo 进程间通信的还有一种方式是fifo. fifo是还有一种管道:有名管道.从名字能够看出.它也是队列. 使用fifo通信前,得先创建fifo $ mkfifo myfifo 随后仅 ...
- JAVA-UML
UML(UnifiedModelingLanguage)(统一建模语言或标准建模语言) 它是模型化的软件系统开发图形语言 为软件开发所有阶段提供模型化,可视化支持 UM2.2中定义了14中图示 三种常 ...
- Laravel技巧之记录多日志
相信每个小伙伴在使用laravel的时候都会记录日志.查看日志.那么问题来了,比如我在对接zabbix接口的时候,使用 Log::info() 会让日志全部记录在 storage/logs/larav ...
- 在dotnet core web api中支持CORS(跨域访问)
最近在写的Office add-in开发系列中,其中有一个比较共性的问题就是在add-in的客户端脚本中访问远程服务时,要特别注意跨域访问的问题. 关于CORS的一些基本知识,请参考维基百科的说明:h ...
- Linux小记 — Ubuntu自动化配置
前言 工欲善其事,必先利其器.经过多次的重复配置ubuntu开发坏境,我终于决定花点时间总结一下,并将其写成一个自动化配置脚本.服务器实例:ubuntu 16.04,技术栈:shell,python. ...
- flask中的session,render_template()第二和参数是字典
1. 设置一个secret_key 2.验证登入后加上session,这是最简单,不保险 . 3.注意render_template传的参数是字典
- NanUI文档 - 打包并使用内嵌式的HTML/CSS/JS资源
NanUI文档目录 NanUI简介 开始使用NanUI 打包并使用内嵌式的HTML/CSS/JS资源 使用网页来设计整个窗口 如何实现C#与Javascript相互掉用(待更新...) 如何处理Nan ...
- NodeJs学习笔记(五)---单元测试补充
今天早上继续研究Mocha,忽然发现一个问题,我的大部分程序都是需要登录验证的,所以需要预先登录之后才能进行下一步测试,就开始在网上找答案,发现没有这种资料,很疑惑,最后发现其实是自己太笨了,因为这个 ...