单例模式

来源:互联网 发布:数据挖掘软件容易使用 编辑:程序博客网 时间:2024/06/11 05:47

说到单例模式,大家或多或少都听说过,尤其是懒汉式和饿汉式,几乎每一个Java程序员都信口拈来,但是却不是每个人都能信手拈来,尤其是信手拈来一个性能良好线程安全的单例模式了。这篇文字旨在总结常见的单例设计模式,并总结一些常跳的坑。

  • 懒汉式
    • 版本一初识懒汉式
    • 版本二方法上锁住你
    • 版本三双重校验锁初级版
    • 版本四双重校验锁终级版
  • 饿汉式
  • 静态内部类
  • 枚举

懒汉式

版本一,初识懒汉式

很多人提到懒汉式都会写下如下版本:

public class Singleton01 {    //我比较懒,先不创建实例    private static Singleton01 instance = null;    private Singleton01(){} // 通过private,禁止其他代码创建该对象    public static Singleton01 getInstance() {        if(instance == null) {            instance = new Singleton01();//到调用创建实例方法的时候再创建并赋值        }        return instance;    }}

甚至有些教科书或者教学视频也是这么教我们的,(笔者曾经看的某培训班视频就是这么写的,很长一段时间都对此深信不疑),但是如果你在面试或者工程中这么写,那无疑在作死。

原因是,这段代码存在一个致命的问题:在初始阶段(instance未被实例化),当有多个线程并行调用 getInstance()方法 的时候,就很可能创建多个实例。也就是说该版本的单例模式在多线程并发的情况下不能正常工作。对,多线程下它压根就不单例。

现在我们想办法改进该版本。

版本二,方法上锁住你

对多线程稍微有点了解的Java工程师在改进上述代码的时候,都会想到如下方法来解决多线程问题:

public class Singleton02 {    private static Singleton02 instance = null;    private Singleton02(){}    //锁住这个方法,简单粗暴    public static synchronized Singleton02 getInstance() {        if(instance == null) {            instance = new Singleton02();        }        return instance;    }}

哈哈!这个简单粗暴的加锁方式肯定是线程安全的了。

但是,它并不高效。不是说锁的粒度太大,而是因为只有在第一次调用getInsance()方法时才会需要同步操作。其余时候再进行同步操作,完全是在浪费性能嘛。

根据这个思路,聪明的程序员们想到了第三种方式。

版本三,双重校验锁初级版

为了让代码更加高效又安全,我们可以使用双重校验锁的模式来实现单例模式:

public class Singleton03 {    private static Singleton03 instance = null;    private Singleton03() {}    public static Singleton03 getInstance() {        if (instance == null) {//一次校验            /* 当对象已经被创建的时候,显然下面的代码不会被执行到 */            synchronized(Singleton03.class) {//当对象还没被创建,加块锁控制可能的并发                if (instance == null) {//两次校验                    instance = new Singleton03();                }            }        }        return instance;    }}

这段代码看起来太巧妙了,甚至可以说完美,即解决了并发安全问题,性能上又没有太大问题。程序员终于可以安心回去睡觉了。
等等!这时经验丰富的长者程序员过来说:你们啊,还是too young。这段代码还有一个地方存在不足

版本四,双重校验锁终级版

长者接着说:问题主要出在了这么一句:

instance = new Singleton03();

要知道,这一句可不是原子操作

JVM在执行这一行语句时其实做了三件事:

  • 1,给 instance 分配内存空间;
  • 2,调用 Singleton03 的构造函数来初始化成员变量;
  • 3,将instance对象指向分配的内存空间(执行完这步 instance 才为非 null 了)

“这一句是不是原子操作又有什么关系呢?反正是放在同步代码块里面,其他线程又无法插入”程序员疑惑的问到。

“此言差矣,因为JVM即时编译器存在指令重排序的优化,上面的步骤2和步骤3的先后顺序是无法保证的。”长者说道。

程序员思索片刻后一拍脑袋:“我明白了,假如是按1->2->3的顺序当然是没什么问题的,但是万一被JVM即时编译器优化成1->3->2的顺序,那就可能出现步骤3执行完毕,该线程的时间片正好结束,被另一个线程抢占执行,此时instance**已经非null**,但是却没有进行过初始化操作,这时编译器就会报错!”

“说的非常对,”长者满意道,“这时我们可以考虑用volatile变量来声明instance”。

“对哦”,程序员恍然大悟,“volatile除了我们常用的使内存对其他线程立即可见以外,还有禁止指令重排的功能呢。这下大功告成了!”

public class Singleton04 {    //用volatile修饰,禁止汇编层指令重排    private volatile static Singleton04 instance = null;    private Singleton04() {}    public static Singleton04 getInstance() {        if (instance == null) {//一次校验            /* 当对象已经被创建的时候,显然下面的代码不会被执行到 */            synchronized(Singleton04.class) {//当对象还没被创建,加块锁控制可能的并发                if (instance == null) {//两次校验                    instance = new Singleton03();                }            }        }        return instance;    }}

高兴之余,不忘划一下重点:

volatile的主要作用有两种

  • 内存的可见性:即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;
  • 禁止指令重排序:汇编层,volatile 变量的赋值操作后面会有一个内存屏障,读操作不会被重排序到内存屏障之前。

饿汉式

这种情况下,单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

public class Singleton05 {    //类加载时就初始化    private static final Singleton05 instance = new Singleton05();    private Singleton() {}    public static Singleton05 getInstance(){        return instance;    }}

“纳尼?这么简单就可以实现线程安全了?看起来性能也不错啊,不早说?”场外传来了不和谐的声音。

这种写法如果完美的话,就没必要在前面啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式,单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法,如果压根就没有用它,就会造成内存的浪费。最重要的是饿汉式的创建方式在一些场景中也将无法使用:譬如果Singleton05实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它。这是,这种方法就不能满足要求。

静态内部类

这种方法是《剑指offer》和《effective Java》都一致推荐的,让我们看看它究竟有多厉害:

public class Singleton06 {      private static class SingletonHolder {          private static final Singleton06 INSTANCE = new Singleton06();      }      private Singleton06() {}      public static final Singleton06 getInstance() {          return SingletonHolder.INSTANCE;     }  }

这种用静态内部类实现单例模式的方法:

  • 1,仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此本质上它是懒汉式的;
  • 2,同时读取实例的时候不会进行同步,没有性能缺陷;
  • 3,不会依赖 JDK 版本。(volatile必须依赖1.5以后的JDK)

枚举

枚举法非常简单:

public enum Singleton07 {    INSTANCE;    public void whateverMethod() {}}
单元素的枚举类型已经成为实现Singleton的最佳方法。 --《effective java》

《effective java》对枚举法有如此好评。是因为枚举有如下优点:

  • 1,可以通过诸如Singleton07.INSTANCE来访问实例。没发现吗,这比调用getInstance()方法可简单多了。
  • 2,创建枚举默认就是线程安全的。
  • 3,防止反序列化导致重新创建新的对象。
原创粉丝点击