在javaeye上看到很多朋友都提出单例模式的一些变种实现,比如加入了即时加载和DoubleCheckLock机制,来提高并发性能。但事实上这些机制真的必要吗?
目前公认影响单例性能的要素有两个:一是实例构造时间开销,一是获取单例实例的同步阻塞开销。
我的理解是,并发相对与同步阻塞的优势,在于当两条线程中的一条在执行时间开销较大的操作,而另一条线程无须执行该操作,则并发执行保证了开销小的线程不需等待开销大的,能正常执行完毕。然而,如果所有线程都只执行一些基本的操作,例如“返回结果”,“变量赋值”,“判断跳转”等,是否并发执行对性能并没有实质性的提升。差别只在于到底在jdk层面在同步块上排队,还是在cpu层面在时间片分配上排队。
对于一个基本的单例模式,在每个jvm中肯定只会发生一次单例构造。同步机制虽然保护的是构造过程,但99%时间锁的是返回结果过程。而对于这种基本操作,有锁和无锁差别真的是这么大(据说有100倍的差别)吗?
网上常见的单例实现有以下几种:
1. 延迟加载的基本实现
public class Singleton {
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2. 即时加载的基本实现
public class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
3. DoubleCheck同步延迟加载
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
举个极端的量化例子,假如有一个单例对象被500条线程使用。单例的构造器中由于使用了一些耗时的资源,执行一次约为1s。但如果单例实例已经创建好,在getInstance()中直接return该实例,耗时为0.1ms。这500条线程中的其中200条会每隔10分钟(以机器内时钟为准)同时访问这个单例对象。
另外,先假定并发操作对于一些基本的操作,例如方法返回,赋值,判断跳转等不会带来性能上的大幅提升(这与JDK的实现有关)。换句话说,200条线程中“返回实例”动作,无论是否在同步块中,总耗时都是0.1*200=20ms。
那么,
第一种情况:首次访问时,第一条线程锁住getInstance(),并执行1s的实际构造逻辑并返回实例(耗时1.0001秒),其他199条线程依次排队。在单例构造好后再依次获取实例,最慢一条线程耗时1.02秒。 二次访问时,所有线程都依次排队获取实例,最慢一条线程耗时0.02秒
第二种情况:在类装载时,构造单例实例耗时1s。首次访问时,200条线程并发获取实例,由于在cpu上还是要排队,最慢一条线程耗时0.02秒。(也就是说,从系统启动到首次访问完成,耗时1.02秒)。二次访问时,所有线程都并发获取实例,最慢一条线程耗时0.02秒。
第三种情况:首次访问时,与第一种情况类似(因为所有线程都通过了instance == null,在同步块上排队),最慢线程耗时1.02秒。二次访问时,所有线程都并发获取实例,但由于增加了条件判断(假设判断跳转时间为1ms),最慢一条线程耗时约为0.04秒。
可以看出,如果假定并发对基本操作时间无影响时,综合性能第一种和第二种一致,第三种最低,但差别均在微秒级别,事实上可以忽略。
需要注意的是,对于一个单例来说,上面例子中的线程总数500是个多余量,对结果完全没有影响。换句话说,是否延迟加载对于单个单例的性能没有影响。
如果假定jdk的实现使得基本操作在并发环境下性能较高,假设返回结果与条件判断的平均性能在并发下能提高1倍(双核?),变成0.05ms。那么,
第一种情况(完全没有利用并发):首次访问最慢线程:1.02秒,二次访问最慢线程0.02秒。
第二种情况:类载入时:1秒,首次访问最慢线程0.01秒(系统启动到完成首次访问总耗时1.01秒),二次访问最慢线程0.01秒
第三种情况:首次访问最慢线程1.01秒,二次访问最慢线程0.02秒。
同样可以看出,三种情况即使有差别,但差别均在微秒级别,事实上可以忽略。而且如果要较真的话,第三种情况还可能会慢,关键在于并发环境下返回结果的性能提升能否抵消多出来的两次判断跳转。
上面讨论的是一个单例被多条线程同时使用的情况。延时读取是考虑的是系统中存在
大量不同单例的某些特殊情况。例如即时加载的劣势,体现在如果系统中有50个不同的单例类,而在某条执行路线上只载入了其中2个,即时加载会导致其他48个单例实例被无谓地创建。问题是在系统中定义了大量的单例类(要注意单例是难以继承的),而主线逻辑中又只使用了其中少量的现实场景究竟有多少。
综上,个人感觉在一般场景中,基本延迟加载方案与基本即时加载方案的效果是差不多的,完全可以根据个人喜好选择。除非已知系统中会出现“单例类爆炸”,而主流程又只使用其中少量的情况,那么就需要在团队中强制推行延迟加载方案。DoubleCheck同步加载方案需要证明“返回结果这种基本操作在并发环境下能带来实质的性能提升,足以抵消额外的条件判断的性能损耗”的前提下,才值得推广。
希望进行过实测的朋友来谈谈经验。
==============================
继续想了一下,似乎在这里使用DoubleCheck方式的主要出发点是”避免同步锁自身的初始化开销”,而不是”避免被锁住内容的执行开销“。那么问题就变为”同步锁自身的初始化开销,是否足以抵消额外的条件判断的性能损耗“。看来还是要实测一下。
分享到:
相关推荐
单例模式单例模式单例模式单例模式单例模式单例模式单例模式单例模式
C#单例模式C#单例模式详解C#单例模式详解C#单例模式详解
单例模式详解~~单例模式详解~~单例模式详解~~
关于单例模式的知识要点: 1、某个类只能有一个实例 2、它必须自行创建这个实例 3、必须自行向这个系统提供这个实例
Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式...
2020-02-10 王争设计模式之美进入课程讲述:冯永吉时长 10:21大小 8.31M上两节课中,我们针对单例模式,讲解了单例的应用场景、几种常见的代码实现
单例模式--只能弹出一个窗体 只能弹出一个窗体
单例模式的七种实现方法以及分析,可以作文大作业提交 1.前言 4 1.1 课题的研究背景 4 1.2 课题主要研究目标 4 2.相关技术简介 4 2.1Java简介 4 2.2IDEA简介 4 3. 单例模式的7种实现方式 5 3.1饿汉式(使用静态常量...
一个简单的java工程,包含注释,一目了然,其中包含了单例模式的所有实现方式,懒汉式,饿汉式,双重校验,枚举,静态内部类等方式实现单例。
php单例模式php单例模式php单例模式php单例模式
单例模式和工厂模式结合应用,实现了产品的生产,适合用做课程设计,包含详细文档和代码。Java语言。喜欢的可以下载来看看那
单例模式是最简单的设计模式之一,但是对于Java的开发者来说,它却有很多缺陷。在本月的专栏中,David Geary探讨了单例模式以及在面对多线程(multithreading)、类装载器(classloaders)和序列化(serialization)时...
几种单例模式的书写方式
设计模式之七种单例模式代码及ppt,包含多线程环境测试和反序列化测试
首先向关注过我这个系列...这立刻让我想到了最常用也是最简单最容易理解的一个设计模式 单例模式 何为 单例模式 ? 故名思议 即 让 类 永远都只能有一个实例。 由于 示例代码 比较简单 我也加了注释,这里就不在赘述
设计模式C++学习之单例模式(Singleton)
单例模式一般在什么场合使用? 是关于单例模式的一个网页
研磨单例模式研磨单例模式研磨单例模式研磨单例模式研磨单例模式研磨单例模式研磨单例模式研磨单例模式研磨单例模式
这个讲的是单例模式的多种不同实现方式,希望对单例感兴趣的同学看看
使用单例模式创建学生管理系统(饿汉式、懒汉式)