单例模式

来源:互联网 发布:淘宝上的杂牌手机 编辑:程序博客网 时间:2024/06/05 18:13

本篇的父博文是:设计模式 - Design Patterns

一定要记得:

设计模式不是创造发明,它只是对某类问题的经验总结,并在此基础上给出的解决问题的最优思路。
它只是在教你代码怎么写,所以,也可能你自己没学过设计模式时就已经想到了某种设计模式那样的写法了


一、单例模式的意义

Design Patterns 一书中对单例模式的整体性描述是这样一句话:

Ensure a class only has one instance, and provide a global point of access to it.
确保一个Class只有一个实例,并对外提供一个全局访问点。

写程序时你总会有这样的需求的,所以单例模式我觉得是最容易理解的模式了,以至于我再多说就会有感觉是在画蛇添足,Just show you the code!

道理简单,不过如何写出一个“正确又好”单例模式一开始并不简单。接下来我主要来描述如何一步一步用Java代码写出好的单例模式。

单例模式的代码有两种不同的方式,一种被叫做所谓的“饿汉式”,另一种叫“懒汉式”。


二、简单方式(所谓“饿汉”)

这种方式及其简单,基本上写不错:

/** * 通常的方式(被叫做所谓“饿汉式”)。 */public class Normal {    private static Normal instance = new Normal();    private Normal() {    }    public static Normal getInstance() {        return instance;    }}

这种方式instance是在classJVM加载后就开始了初始化。

另一种需求:

有时候你有另一种需求:“就是你不想在类加载的时候就实例化”,你希望当有线程调用getInstance()时再去实例化这个对象,也即让程序“懒”加载的需求。这在某class的实例非常消耗资源,也许程序运行很久后才会被调用到的情况下非常有用。

所以我们,接下来主要看懒加载的方式。


三、懒加载方式(所谓“懒汉”)

1. 错误写法一

你可能会轻易的写出如下的懒加载代码来,不过显然如下代码是有线程安全问题的:当线程Thread1执行到// 1然后丢失了CPU执行权时,Thread2显然会判断instance == null继而执行初始化;之后Thread1重新获取执行权后必然会再次初始化一个实例并赋值给instance

/** * 延迟加载的方式(被叫做所谓“懒汉式”)。 * <p>注意这是一个错误的写法,线程不安全,安全写法见${@link LazyInitializationCorrect}</p> */public class LazyInitializationError {    private static LazyInitializationError instance;    private LazyInitializationError() {    }    public static LazyInitializationError getinstance() {        if (instance == null) {            // 1            instance = new LazyInitializationError();        }        return instance;    }}

2. 错误写法二

你可能会这样改进你的代码,事实上这被称为Double-checked locking。这个写法也是有问题的,可以在代码的注释部分详细看到分析与解决方案。我会在下面的# 3中将完全正确的代码放上。

/** * 延迟加载的方式(被叫做所谓“懒汉式”)。 * * <p>虽然使用了“Double-checked locking”,但注意这是又一个错误的写法,错误描述见代码注释 * <br>安全写法见${@link LazyInitializationCorrect}</p> */public class LazyInitializationErrorNoVolatile {    private static LazyInitializationErrorNoVolatile instance;    private LazyInitializationErrorNoVolatile() {    }    public static LazyInitializationErrorNoVolatile getinstance() {        // Double-checked locking        if (instance == null) {// first check            synchronized (LazyInitializationErrorNoVolatile.class) {                if (instance == null) {                    instance = new LazyInitializationErrorNoVolatile();                    // 上面这句代码在JVM中被分解为3个指令                    // 1. 给 instance 分配内存(假设内存地址为:10001)                    // 2. 调用 LazyInitializationErrorNoVolatile()构造函数初始化 instance                    // 3. 将instance对象指向分配的内存空间(即10001,执行完这步 instance 已经 非null )                    // 问题:                    // 1. 由于JVM的JIT编译器存在着称为“指令重排序”的优化逻辑,所以上面这行代码的指令执行顺序并非一定是“1-2-3”,有可能被优化为“1-3-2”;                    // 2. 这正是问题所在,如果Thread1执行指令的顺序为“1-3-2”的话,有可能在执行到“1-3”时丢失了CPU的执行权,此时由于3已执行所以instance为非null;                    // 3. Thread2获取执行权,当执行到 first check 时,发现结果为false,所以直接执行最后一句return instance,然而此时instance指向的对象并没有执行构造函数,随发生错误。                    // 解决问题:                    // 其实很简单,使用volatile关键字即可。因为volatile有个特性是:阻止执行重排序优化,使得上面执行的顺序必须是“1-2-3”即可。                }            }        }        return instance;    }}

3. 正确写法

其实只是对# 2的代码做了一个修饰符的改进,即volatile

/** * 延迟加载的方式(被叫做所谓“懒汉式”)。 * * <p>完全线程安全</p> */public class LazyInitializationCorrect {    private static volatile LazyInitializationCorrect instance;    private LazyInitializationCorrect() {    }    public static LazyInitializationCorrect getinstance() {        // Double-checked locking        if (instance == null) {            synchronized (LazyInitializationCorrect.class) {                if (instance == null) {                    instance = new LazyInitializationCorrect();                }            }        }        return instance;    }}

事实上,懒加载还有一种效率更高的完全正确的写法如下:

4. 另一种正确写法

这种方式叫 Initialization-on-demand holder idiom,这种方式效率很高因为没有同步代码,当然它也有一定的局限性,这些我在注释中都详细的写了出来:

/** * 使用Hodler方式(被叫做所谓“懒汉式”)。 * * <p>完全线程安全,“Initialization-on-demand holder idiom”,但有一定限制</p> */public class LazyInitializationCorrectWithHolder {    private LazyInitializationCorrectWithHolder() {    }    // 1. LazyInitializationCorrectWithHolder被JVM加载时,如果Holder类没被任何代码引用到的话,是不会被加载的,所以这里满足了“懒”;    private static class Holder {        private static final LazyInitializationCorrectWithHolder INSTANCE = new LazyInitializationCorrectWithHolder();    }    public static LazyInitializationCorrectWithHolder getInstance() {        // 2. 当外部代码调用 getInstance() 时,Holder才会被加载并执行;        // 3. 又由于JVM保证了class的初始化是按照既定顺序的,不会多线程的,所以 INSTANCE 的初始化是线程安全的。        return Holder.INSTANCE;    }    // 4. 使用这种风格的单例,必须满足一个条件,构造函数的过程不能发生异常导致初始化失败。(double checked方式没有这个限制)    // 错误代码如下,当初始化失败后,其它线程也不能再次完成初始化了,因为会一直发生LazyInitializationCorrectWithHolder$Holder NoClassDefFoundError    // private LazyInitializationCorrectWithHolder() {    //     if (Thread.currentThread().getName().equals("Threadddd-0")) {    //         int i = 1/0;    //     }    // }    // public static void main(String[] args) {    //     for (int i = 0; i < 3; i++) {    //         final Thread thread = new Thread(new Runnable() {    //             @Override    //             public void run() {    //                 getInstance();    //             }    //         });    //         thread.setName("Threadddd-" + i);    //         thread.start();    //     }    // }}

以上,我们最终写出了两种很好的懒加载方式的单例模式代码,没有比较难理解的点,不过如果你一开始没想到一些潜在的问题,其实也很难写出正确的代码。看过了这篇文章,相信你再也写不错了。


转载注明出处:http://blog.csdn.net/u010297957/article/details/70544003

1 0
原创粉丝点击