临近年底,没有开发任务,so写写博客,也算是总结回顾下知识。近期主要是从代码规范角度梳理知识,如最近写的设计模式系列。设计模式是我们前辈通过开发中遇到的一系列的问题,经过相当长的一段经验总结出来的一套通用的技术解决方案。开发中最常见的、也是大多数人经历的第一种设计模式,非单例设计模式莫属了。
今天就来分析一下单例设计模式。写单例的文章有很多,也是我自认为掌握的最好的设计模式,然而很遗憾,我一开始就被误导了。
单例,顾名思义,我要保证的是,它在内存中是唯一的实例。老样子,从最简单的单例开始。
1.饿汉式
/**
* 描述:单例设计模式 - 饿汉式
*
* @author yangjiaming
* @date 2018/2/6
*/
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
这算是最简单的单例,也是大家都耳熟能详的代码了。1)私有构造方法; 2)创建私有静态的对象; 3) 创建公共方法返回得到的对象。
它是通过classloader机制避免了线程同步问题,保证了他在内存中唯一的同时,也存在着很大的弊端。类一加载的时候对象就被创建,没有达到lazyloading的效果。所以,又引出了懒汉式。
2.懒汉式(线程不安全)
/**
* 描述:单例设计模式 - 懒汉式,线程不安全
*
* @author yangjiaming
* @date 2018/2/6
*/
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这是一个最基础的懒汉式单例,弊端我们也都知道,没有保证线程同步。保证同步的办法也很简单,即在getInstance()方法上添加synchronized关键字。这样即保证了线程的同步。但它也带来了很大的开销,严重影响力程序的执行效率。
3.懒汉式(线程安全,且延迟加载)
/**
* 描述:单例设计模式 - 懒汉式,double checked
*
* @author yangjiaming
* @date 2018/2/6
*/
public 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;
}
}
从这里开始,就进入本文的重点了。
之前的学习中,这种方式是针对多线程中使用单例,最优化的。第一个判空是必不可少的,如果当前线程在调用getInstance()时已经创建了对象,那么就直接返回,不需要每个线程都去频繁的判断锁的操作。第二个判空,是考虑到两个或者两个以上的线程经过了第一个判空条件,有一个线程拿到锁去创建对象,而其他线程在等待锁的释放这种情况,如果没有这层判空,会导致多个实例被创建。
上面的代码,对吗?
回答之前,先看一下内存定义操作顺序:
这里讲的就是,对任意一个线程在退出同步块之前都能看到任何内存操作,在进入同一个监视器保护的同步块后,任何线程都可见,因为所有内存操作都发生在”release”之前,而释放发生在”acquire”之前。这样就存在着相当大的隐患!也就是说释放和获取,并没有在同一个监视器上同步,相当于不同线程间存在着一个数据竞赛,最终的字段也无法保证唯一性:
引入volatile关键字:
现在再去回顾初识的double-checked的懒汉式:
// 假装有代码
这看起来非常聪明,在通用代码路径上避免了同步。但有一个问题,它不起作用。为什么不?最明显的原因是,初始化实例和写入实例字段的写入操作可以由编译器或缓存重新排序,这会产生返回看起来是部分构造的东西的效果。结果是我们读取一个未初始化的对象。详情:double-checked-locking:聪明,但破碎,double-checked-locking已损坏说明。
加入volatile关键字后,就解决了上面所说的这个问题。因为volatile保证了对象在”release”和”acquire”的匹配。但是,double-checked最重要的目的就是在于避免了同步问题出现的性能开销,而在新的Java内存模型【JSR 133】定义下,volatile的性能成本几乎上升到了与synchronized同等水平。所以没必要再去使用这种方式。
4.通过静态内部类方式创建单例
/**
* 描述:单例设计模式 - 静态内部类方式
*
* @author yangjiaming
* @date 2018/2/6
*/
public class Singleton {
private Singleton() {
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
因为静态关键字修饰,利用了classloder机制保证了对象在内存中的唯一实例,又SingletonHolder并未被调用,只有调用getInstance()方法时,才会装载内部类,巧妙地实现了懒加载。
当然,实现单例的方式还有多种,比如有的文章中指出的枚举方式实现。但是这种方式,是我目前来说最常用的。