设计模式(1) - 单例

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mytt_10566/article/details/85888437

参考:<<Java并发编程的艺术>>

单例模式有以下特点:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态。

1. 饿汉式

在类初始化时直接实例化,没有线程安全问题,简单粗暴。

public class Singleton {
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return singleton;
    }
}

2. 懒汉式

我们有时候需要采用延迟初始化来降低初始化类和创建对象的开销。一般采用双重检查锁定来延迟初始化,实现线程安全的延迟初始化需要一些技巧。

2.1 同步synchronized

对getInstance()方法做了同步处理,synchronized将会导致性能开销。如果该方法被多个线程频繁调用,将会导致程序执行性能下降;如果没有被多个线程频繁调用,那么这种方式也是可行的。

public class SyncSingleton {
    private static SyncSingleton instance;

    private SyncSingleton() {
    }

    public static synchronized SyncSingleton getInstance() {
        if (instance == null) {
            instance = new SyncSingleton();
        }
        return instance;
    }
}

2.2 多重检查锁定

在JDK1.6之前的JVM中,synchronized存在巨大的性能开销。因此,出现了双重检查锁定(Dobule-Checked- Locking),想通过这种方式来降低同步的开销。示例:

public class DCLSingleton {
    private static DCLSingleton instance;

    private DCLSingleton() {
    }

    public static DCLSingleton getInstance() {
        if (instance == null) {                // 1
            synchronized (DCLSingleton.class) {// 2
                if (instance == null) {        // 3
                    instance = new DCLSingleton();// 4可能会有问题
                }
            }
        }
        return instance;
    }
}

上面的代码表面看起来没有问题:

  • 多个线程调用时,通过加锁来保证只有一个线程能创建对象
  • 在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回创建好的对象

实际上在执行第1行时,发现instance不为null,但是instance引用的对象有可能还没有完成初始化。因为第4行实例化可以分解为以下3行伪代码:

 上面的2和3之间,可能会被重排序。2和3重排序之后执行顺序如下:

这种情况在单线程环境下不会有问题,并不影响最终的结果;但是在多线程环境下,就有可能instance非空,但还没初始化完成。

在知道原因之后,可以有两种方法实现线程安全的延迟初始化:

  • 不允许2和3重排序
  • 允许2和3重排序,但不允许其他线程“看到”这个重排序

(1)基于volatile关键字

上面示例中,只需要用volatile修饰instance,不允许2和3重排序,就可以实现线程安全的延迟初始化。

注:需要使用JDK5及以上版本,因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义(禁止指令重排序)

public class DCLSingleton {
    private static volatile DCLSingleton instance;

    private DCLSingleton() {
    }

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

(2)基于类初始化

JVM在类的初始化阶段(即Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM回去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

基于这个特性,可以实现另一种线程安全的延迟初始化方案,称为Initialization On Demand Holder idiom。示例:

public class ClassloadSingleton {

    private ClassloadSingleton() {
    }

    private static class InstanceHolder {
        public static ClassloadSingleton instance = new ClassloadSingleton();
    }

    public static ClassloadSingleton getInstance() {
        return InstanceHolder.instance;
    }
}

这个方案利用类初始化阶段有锁的特点,即使在实例化过程中发生了重排序,也不会导致初始化前被其他线程拿到引用,因为其他线程无法获取锁只能等待,直到持有锁的线程初始化完成。这就是允许2和3重排序,但不允许其他线程“看到”这个重排序。

具体的初始化过程可以参考书中p73-78说明。

猜你喜欢

转载自blog.csdn.net/mytt_10566/article/details/85888437
今日推荐