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 ...
随机推荐
- python使用rsa库做公钥解密(网上别处找不到)
使用RSA公钥解密,用openssl命令就是openssl rsautl -verify -in cipher_text -inkey public.pem -pubin -out clear_tex ...
- FastDFS教程Ⅲ-文件服务器扩容
1.简介 FastDFS文件服务器在设计时,为了支持大容量,存储节点(服务器)采用了分卷(或分组)的组织方式.存储系统由一个或多个卷组成,卷与卷之间的文件是相互独立的,所有卷的文件容量累加就是 ...
- 【深度学习系列】用PaddlePaddle和Tensorflow实现经典CNN网络Vgg
上周我们讲了经典CNN网络AlexNet对图像分类的效果,2014年,在AlexNet出来的两年后,牛津大学提出了Vgg网络,并在ILSVRC 2014中的classification项目的比赛中取得 ...
- 网页设计——7.css的入门
css的介绍 div+css的设计: 什么是div? 理解示意图: 实例操作: 这里就要用到div+css的布局操作 先写一个html文件,见下图: <html><head>& ...
- NOIP2017 小凯的疑惑
题目描述 小凯手中有两种面值的金币,两种面值均为正整数且彼此互素.每种金币小凯都有 无数个.在不找零的情况下,仅凭这两种金币,有些物品他是无法准确支付的.现在小 凯想知道在无法准确支付的物品中,最贵的 ...
- 洛谷 P3927 Factorial
题目描述 SOL君很喜欢阶乘.而SOL菌很喜欢研究进制. 这一天,SOL君跟SOL菌炫技,随口算出了n的阶乘. SOL菌表示不服,立刻就要算这个数在k进制表示下末尾0的个数. 但是SOL菌太菜了于是请 ...
- 简陋的斗地主,js实现
最近闲了两天没事做,用js写了个斗地主,练习练习.代码和功能都很简陋,还有bug,咋只是聊聊自己的思路. 这里说说斗地主主要包含的功能:洗牌,发牌,玩家出牌.电脑出牌,出牌规则的验证,输赢啥的没有判断 ...
- Scala入门系列(十一):模式匹配
引言 模式匹配是Scala中非常有特色,非常强大的一种功能. 类似于Java中的switch case语法,但是模式匹配的功能要比它强大得多,switch只能对值进行匹配,但是Scala的模式匹配除了 ...
- day8、 显示Linux路由表、各列信息
要用到的命令是 route route 命令 显示和设置Linux路由表 -A:设置地址类型: -C:打印将Linux核心的路由缓存: -v:详细信息模式: -n:不执行DNS反向查找,直接显示 ...
- arguments.callee的临时指向特性
function r(){ alert('BBB'); } var a = { f: function(){ alert('AAA'); arguments.callee = r; } }; 弹出的都 ...