Java-----关于单例设计模式

末鹿安然 提交于 2020-01-30 18:50:23

1. 单例模式DCL写法

单例设计模式中,有一种双重检查锁的写法, 也就是所谓的懒汉式

class Single{
  private static Single sSingle;

  private Single() {}

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

}

2.DCL写法的问题

这种写法,如果使用sonar代码检测工具,会有警告提示,具体描述如下

Double-checked locking is the practice of checking a lazy-initialized object's state both before and after a synchronized block is entered to determine whether or not to initialize the object.

It does not work reliably in a platform-independent manner without additional synchronization for mutable instances of anything other than float or int. Using double-checked locking for the lazy initialization of any other type of primitive or mutable object risks a second thread using an uninitialized or partially initialized member while the first thread is still creating it, and crashing the program.

There are multiple ways to fix this. The simplest one is to simply not use double checked locking at all, and synchronize the whole method instead. With early versions of the JVM, synchronizing the whole method was generally advised against for performance reasons. But synchronized performance has improved a lot in newer JVMs, so this is now a preferred solution. If you prefer to avoid using synchronized altogether, you can use an inner static class to hold the reference instead. Inner static classes are guaranteed to load lazily.                        

大概意思是说

使用双重检查锁定可能会导致第二个线程使用未初始化或部分初始化的成员,而第一个线程仍在创建它,并使程序崩溃。

有多种方法可以解决这个问题。最简单的方法是根本不使用双重检查锁定,而是同步整个方法。对于早期版本的JVM,出于性能原因,通常建议不要同步整个方法。

但是,在较新的jvm中,同步性能有了很大的提高,所以现在这是一个首选的解决方案。如果希望避免使用synchronized,可以使用内部静态类来保存引用。内部静态类被保证可以延迟加载。

所以DCL单例的写法是不推荐的

 

在《Java并发编程实践》 中对于这种写法,有这么一段描述

找了一些资料,都对此问题有大致描述, 简单来讲:

假设线程A 执行到sSingle = new Single()语句, 这里看起来是一句代码,但实际上它并不是一个原子操作, 这句代码最终会被编译成多条汇编指令, 它大致做了3件事情

(1) 给Single的实例分配内存;

(2) 调用Single()的构造函数, 初始化成员字段;

(3) 将sSingle 对象指向分配的内存空间( 此时sSingle才不为null )

但是, 由于Java编译器允许乱序执行, 以及JDK1.5之前JMM 中Cache、寄存器到主内存回写顺序的规定, 上面的 (2) 和 (3) 的顺序是无法保证的。也就是说, 执行顺序可能是1-2-3,也可能是1-3-2。

如果是后者, 并且在(3)执行完毕、(2) 未执行之前,被切换到线程B上,这个时候sSingle因为已经在线程A内执行过了第三点, sSingle已经 非null状态, 所以,线程B直接取值sSingle,再使用时就会出错,

这就是DCL失效问题,而且这种难以跟踪难以重现的错误可能会隐藏很久

                                                      -----摘自《Android源码设计模式解析与实战》

在JDK1.5之后,SUN官方已经注意到这种问题,调整了volatile关键字,如果在JDK1.5之后的版本,只需将写法改成

class Single{  // 此处加上volatile
  private volatile static Single sSingle;

  private Single() {}

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

}

关于volatile关键字, 多少会影响一些性能, 但为了程序的正确性,牺牲这点性能是值得的

 

3. 单例设计模式推荐写法

对于单例设计模式,推荐下方写法

public class Single {

    private Single() {
    }

    public static Single getSingle() {
        return SingleHolder.holder;
    }

    private static class SingleHolder {
        private static final Single holder = new Single();
    }
}

 

4. 附录

1. 关于DCL失效问题的原理方面, 这篇文章介绍的比较详细, 推荐看一下

  Java并发---DCL失效问题  

 

2. 关于JMM, volatile关键字, 这篇文章非常详细

  java并发编程:volatile关键字解析

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!