单例模式

说到单例模式,大家应该都不陌生,毕竟它是应用最广泛的模式之一。

单例模式的主要实现形式

饿汉模式

饿汉模式是在声明静态对象时就已经初始化单例了。代码如下:

public class Singleton {

    private static Singleton mInstance = new Singleton();

    public static Singleton getInstance() {
        return mInstance;
    }

    private Singleton() {}
}

缺点:无论使用还是不使用都会初始化,造成不必要的开销。

懒汉模式

懒汉模式实在第一次调用getInstance的时候去初始化。代码如下:

public class Singleton {

    private static Singleton mInstance;

    public static synchronized Singleton getInstance() {
        if (mInstance == null) {
            mInstance = new Singleton();
        }
        return mInstance;
    }

    private Singleton() {
    }
}

优点:只有在第一次使用的时候才会去实例化单例。

缺点:每次调运都会去同步,造成不必要的同步开销。

double check lock(DCL)实现单例

DCL实现单例的优点是既能在需要的时候才初始化单例,又能保证线程安全,而且单例初始化后,调用getInstance没有同步开销。代码如下:

public class Singleton {

    private static Singleton mInstance;

    public static Singleton getInstance() {
        if (mInstance == null) {
            synchronized (Singleton.class) {
                if (mInstance == null) {
                    mInstance = new Singleton();
                }
            }
        }
        return mInstance;
    }

    private Singleton() {
    }
}

但是,这种模式存在缺陷,就是在高并发的情况下会出问题。这是为什么了?下面来分析一下。

mInstance = new Singleton();它不是一个原子操作,这个行代码最终会被编译成汇编指令,它大致做了三件事:

  1. 给Singleton实例分配内存空间
  2. 调运Singleton()初始化
  3. 将mInstance指向分配好的内存空间(这时mInstance就不为null了)

但是jvm为了优化指令,提高运算效率就会进行指令重排,导致2、3不一定是顺序执行的。也就是说执行的顺序可能是1-2-3,也可能是1-3-2。这就尴尬了,当A线程执行了1-3,此时mInstance已经不为null了,但是它指向的内存是不可用的,此时B线程调运了getInstance(),返现mInstance不为null,所以就直接使用了,这时就会出错。这就是DCL失效的问题,而且这种难以追踪难以重现的问题会隐藏很久。

指令重排是什么?下面来介绍一下

指令重排

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。

不同的指令间可能存在数据依赖。比如下面计算圆的面积的语句:

double r = 2.3d;//(1)
double pi =3.1415926; //(2)
double area = pi* r * r; //(3)

area的计算依赖于r与pi两个变量的赋值指令。而r与pi无依赖关系。

as-if-serial语义是指:不管如何重排序(编译器与处理器为了提高并行度),(单线程)程序的结果不能被改变。这是编译器、Runtime、处理器必须遵守的语义。

虽然,(1) – happens before -> (2),(2) – happens before -> (3),但是计算顺序(1)(2)(3)与(2)(1)(3) 对于r、pi、area变量的结果并无区别。编译器、Runtime在优化时可以根据情况重排序(1)与(2),而丝毫不影响程序的结果。

指令重排序包括编译器重排序和运行时重排序。

防止指令重排

JDK1.5之后sun公司注意到了这个问题,就增加了volatile。可以使用volatile变量禁止指令重排序。变量在以volatile修饰后,会阻止JVM对与其相关的代码进行重排,达到按照既定顺序执行代码的目的。代码如下:

public class Singleton {

    private static volatile Singleton mInstance;

    public static Singleton getInstance() {
        if (mInstance == null) {
            synchronized (Singleton.class) {
                if (mInstance == null) {
                    mInstance = new Singleton();
                }
            }
        }
        return mInstance;
    }

    private Singleton() {
    }
}

静态内部类单例模式

DCL虽然解决了资源消耗、多余同步、线程安全等问题,但是在某些情况下,它还会出现实效的情况。在《java并发编程》一书中指出DCL是一种丑陋的写法,不赞成使用。并建议使用如下写法:

public class Singleton {

    public static Singleton getInstance() {
        return SingletonHolder.mInstance;
    }

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton mInstance = new Singleton();
    }
}

但加载Singleton类的时候,并不会去初始化mInstance,只有第一次调用getInstance的时候在回去初始化mInstacnce。第一次调用getInstance的时候,会导致虚拟机去加载SingletonHolder类,这时才会初始化mInstance。

你可能感兴趣的:(单例模式)