单例模式,即是整个类有且只有一个类实例,通过这个唯一的实例为全局提供服务。单例类的构造方法为私有的,通过一个暴露给外界的获取实例方法来调用私有构造方法,保证实例的唯一性。
1,饿汉式单例,在方法调用前就初始化了实例(线程安全的)
// 因为饿汉式单例在类加载时就初始化了唯一的实例(且只会初始化一次,所以,该实例是唯一的,即 线程安全的),因此,如果我们想要为实例设定一些参数或配置时,将变得很困难。这就是其局限性
public class Singleton {
private static Singleton mInstance = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return mInstance;
}
}
2,(线程不安全的)懒汉式单例,即在获取实例方法内才初始化实例。
public class Singleton {
private static Singleton mInstance = null;
private Singleton(){}
// 当多线程并发时,无法保证实例的唯一性。为了保证线程安全,可以为方法或代码块加上synchronized关键字
public static Singleton getInstance() {
if (mInstance == null) {
mInstance = new Singleton();
}
return mInstance;
}
}
3,(线程安全的)懒汉式单例(两种方式),
使用了同步后,是线程安全了,但是效率却也变低了。因为每次线程获取实例时,都要进行方法或代码块的同步准备。(使用synchronized关键字是比较耗时的)为了不那么耗时,我们可以使用DCL(二次检查锁定)
public class Singleton {
private static Singleton mInstance = null;
private Singleton(){}
// 第一种,为方法加上synchronized关键字
public synchronized static Singleton getInstance() {
if (mInstance == null) {
mInstance = new Singleton();
}
return mInstance;
}
public static Singleton getInstance() {
// 第二种,为创建实例的代码块加上synchronized关键字
synchronized (Singleton.class) {
if (mInstance == null) {
mInstance = new Singleton();
}
}
return mInstance;
}
}
4,(线程安全且不那么耗时)懒汉式单例,DCL方式(二次检查锁定)。
DCL其实是有问题的。因为JVM内存模型(JMM)允许‘无序写入’。即在JVM执行指令过程中,指令的执行顺序有可能是乱序的。如代码mInstance = new Singleton();其实这行代码做了3件事:
1,为单例对象分配内存空间
2,将mInstance引用变量指向刚分配好的内存空间(此时,mInstance已经是非null的了)
3,为单例对象通过mInstance调用其类的构造方法进行初始化。
在JVM执行过程中,2,3步的执行顺序是不确定的,可能是颠倒的。颠倒的情况下,在并发时,就有可能发生严重的错误,当线程一执行到步骤2而未执行步骤3时(实例未初始化,但引用变量mInstance已经非null),如果此时被线程2获取到了CPU的使用权,线程2在执行mInstance的非null判断时,将会认为mInstance为非null而直接返回引用,但其实此时是未初始化的,如果线程2使用这个mInstance引用,系统就会报错(因为mInstance未初始化却被使用了)。
在JDK版本较高(>1.5)的情况下,可以通过使用volatile关键字来避免这种情况
public class Singleton {
// 为了配合DCL,一般需要用volatile关键字修饰实例变量。volatile关键字保证了其被创建后会立即反映到JVM的主存中(即立即更新到主存中),其被读取也是线程立即到主存中去读取,而不经过线程的缓存,保证了实例的状态在任何时刻都是最新的
// 更多关于volatile关键字的理解可以参考[volatile关键字](http://www.importnew.com/18126.html)
private volatile static Singleton mInstance = null;
private Singleton(){}
// 为什么会不那么耗时昵?因为我们除了在第一次创建实例时进行了代码块的同步准备外,其他的访问,会在第一次判断时就会返回(此时实例已不为null),不会进行同步准备,而是直接返回了实例,所以提高了效率
public static Singleton getInstance() {
// 第一次判断(检查)
if (mInstance == null) {
synchronized (Singleton.class) {
// 第二次判断(二次检查)
if (mInstance == null) {
mInstance = new Singleton();
}
}
}
return mInstance;
}
}
5,优化DCL
public class Singleton {
private static Singleton mInstance = null;
private Singleton(){}
public static Singleton getInstance() {
// 第一次判断(检查)
if (mInstance == null) {
synchronized (Singleton.class) {
// 第二次判断(二次检查)
if (mInstance == null) {
Singleton temp = null;
try {
temp = new Singleton();
} catch(Exception e) {}
if (temp != null)
// 为什么要做这个看似无用的操作,因为这一步是为了让虚拟机执行到这一步的时才会对singleton赋值,
// 虚拟机执行到这里的时候,必然已经完成类实例的初始化。所以这种写法的DCL是安全的。由于try的存在,虚拟机无法优化temp是否为null
// 这里需要了解try关键字
mInstance = temp;
}
}
}
return mInstance;
}
}
6,静态内部类实现单例
// 因为通过静态内部类实现,当某个线程要获取实例时,return SingletonHolder.INSTANCE这个语句会通知JVM进行内部类的加载(只会加载一次,这意味着INSTANCE变量会且只会被初始化一次,所以是线程安全的)。当实例被首次初始化后,其他线程访问的将会是同一个实例
// 类加载发生在需要的时候。通俗地讲,只有在程序执行到需要某个类的时候(如读取某个属性值),才会进行该类的加载。所以,当一个类被加载时,它的内部类不会被同时加载。
public class Singleton {
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static Singleton getInstance() {
// 当内部类的属性被访问时,内部类才会被加载,所以这是懒汉式单例
return SingletonHolder.INSTANCE;
}
}
7,枚举实现单例
// 单元素的枚举类。
// 使用枚举实现单例模式可以避免通过反射以及反序列化破坏单例的问题
public enum EnumSingleton{
INSTANCE;
private EnumSingleton() {}
}
本文深入解析单例模式的实现方式,包括饿汉式、懒汉式(线程安全与不安全)、DCL优化及静态内部类等。探讨了每种方式的特点、优缺点及适用场景,特别关注线程安全性和效率之间的平衡。
996

被折叠的 条评论
为什么被折叠?



