【设计模式.创建型】4种单例模式-彻底了解双重加锁机制

时间:2021-06-20 20:50:21

1、基本介绍:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

2、用途:

应用中某个实例对象需要频繁的被访问。
应用中每次启动只会存在一个实例。如账号系统,数据库系统。

3、实现方式:

3.1、lazy instantiaze 懒加载

public class Demo1 {
    private Demo1() { }
    private static Demo1 demo = null;
    public static Demo1 getInstance() {
        if (demo == null) {      //@1
            demo = new Demo1();  //@2
        }
        return demo;
    }
}
  • 优点:延迟加载(需要的时候才会加载)
  • 缺点:多线程容易出现不同步的问题。

假设这样的场景:两个线程并发调用Demo1.getInstance(),假设线程一先判断完demo是否为null,既代码中的@1进入到@2的位置。刚刚判断完毕后,JVM将CPU资源切换给线程二,由于线程一还没执行@2,所以demo仍然是空的,因此线程二执行了new
Demo1()操作。片刻之后,线程一被重新唤醒,它执行的仍然是new Demo1()操作。

3.2、饿加载:

public class Demo2 {
    private Demo2(){}
    private static Demo2 demo2 = new Demo2();
    public static Demo2 getInstance() {
        return demo2;
    }
}
  • 优点:不会出现多线程访问问题,依赖JVM在加载这个类的时候马上创建唯一的单例实例,JVM保证在任何线程访问单例变量之前,一定先创建此单例。
  • 缺点:
    1.如果实例开销较大,而且程序中未使用,性能损耗。
    2.如实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

3.3、同步锁synchronized:

public class Demo3 {
    private Demo3() {
    }
    private static Demo3 demo3 =null;
    public synchronized static Demo3 getInstance() {
        if (demo3 == null) {
            demo3 = new Demo3();
        }
        return demo3;
    }
}
  • synchronized :保证了并发编程的3大特性,迫使每个线程在进入这个方法之前,要先等到别的线程离开该方法,不会有2个线程同时进入到这个方法中。【详细用法见synchronized章节】
  • 优点:解决了多线程并发访问的问题。
  • 缺点:性能问题。 只有第一次执行此方法的时候才需要同步,一旦设置了demo3变量,就不需要同步这个方法。此后的每次操作都会消耗在同步上。

3.4、双重加锁机制:【最复杂,最难理解,需要提前了解java中的内存模型、Volatile跟synchronized机制】

public class Demo4 {
    private Demo4() { }
    private volatile static Demo4 demo4 = null;
    public static Demo4 getInstance() {
        if (demo4 == null) {
            synchronized (Demo4.class) {
                if (demo4 == null) {
                    demo4 = new Demo4();    //@1
                }
            }
        }
        return demo4;
    }
}
  • 优点:多线程访问不会出现不同步问题,并且减缓了3.3同步锁单例的性能弊端。
  • 难点1:synchronized能保证可见性,为什么单例模式还需要加volatile关键字?
  • 解答1:
    这里加锁确实能够保证这个对象不会被new两次,但不能保证对象的创建过程不被重排序:
demo4 = new Demo4(); //实例化类@1并不是一个原子操作,这一行代码可以分解为如下的三行伪代码:
memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory);  //2:初始化对象
demo4 = memory;     //3:设置demo4变量指向刚分配的内存地址

上面三行伪代码中的2和3之间,可能会被重排序,造成2中的构造方法操作有可能没有执行完,3中的demo4就拿到了这个值,从而出现异常问题。

假想一个场景:线程1进入到@1位置进行开始实例化操作,由于编译器的指令重排序,它是按照1、3、2步骤进行的;但是在执行完第3步还没有执行到第2步时,demo4变量已经拿到值,指向了堆内存中的地址,此时由于锁的可见性,线程2工作内存中的demo4备份值已经更改,会去主存重新读取demo4的值,并且返回了有值的demo4,但是此时实际上Demo4类并没有完成实例化,Demo4对象并没有生成,所以此时会出现问题。

当声明对象的引用为volatile后,“问题的根源”的三行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止,类在实例化过程中会严格按照1、2、3顺序执行下去。

4、注意点:

• 单例模式不会被jvm垃圾回收的
• 实现单例模式需要私有构造器、一个静态变量、一个静态方法
• 使用多个类加载器时,可能导致单例失效,产生多个实例,解决:自行指定类加载器,并指定同一个类加载器。

参考文章:http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization