zoukankan      html  css  js  c++  java
  • 【深入】java 单例模式(转)

    public class Singleton{  
       private static Singleton instance = null;//是否是final的不重要,因为最多只可能实例化一次。  
       private Singleton(){}  
       public static Singleton getInstance(){  
           if(instance == null){  
               //双重检查加锁,只有在第一次实例化时,才启用同步机制,提高了性能。  
               synchronized(Singleton.Class){  
                   if(instance == null){  
                       instance = new Singleton();  
                   }  
               }  
           }  
           return instance;  
       }  

    面试只要会写这一种就ok了

    java单例模式(双重检查加锁)

     

     

     

    【深入】java 单例模式

      关于单例模式的文章,其实网上早就已经泛滥了。但一个小小的单例,里面却是有着许多的变化。网上的文章大多也是提到了其中的一个或几个点,很少有比较全面且脉络清晰的文章,于是,我便萌生了写这篇文章的念头。企图把这个单例说透,说深入。但愿我不会做的太差。

      首先来看一个典型的实现:

    复制代码
     1 /**
    2 * 基础的单例模式,Lazy模式,非线程安全
    3 * 优点:lazy,初次使用时实例化单例,避免资源浪费
    4 * 缺点:1、lazy,如果实例初始化非常耗时,初始使用时,可能造成性能问题
    5 * 2、非线程安全。多线程下可能会有多个实例被初始化。
    6 *
    7 * @author laichendong
    8 * @since 2011-12-5
    9 */
    10 public class SingletonOne {
    11
    12 /** 单例实例变量 */
    13 private static SingletonOne instance = null;
    14
    15 /**
    16 * 私有化的构造方法,保证外部的类不能通过构造器来实例化。
    17*/
    18 private SingletonOne() {
    19
    20 }
    21
    22 /**
    23 * 获取单例对象实例
    24 *
    25 * @return 单例对象
    26*/
    27 public static SingletonOne getInstance() {
    28 if (instance == null) { // 1
    29 instance = new SingletonOne(); // 2
    30 }
    31 return instance;
    32 }
    33
    34 }
    复制代码

      注释中已经有简单的分析了。接下来分析一下关于“非线程安全”的部分。

      1、当线程A进入到第28行(#1)时,检查instance是否为空,此时是空的。
      2、此时,线程B也进入到28行(#1)。切换到线程B执行。同样检查instance为空,于是往下执行29行(#2),创建了一个实例。接着返回了。
      3、在切换回线程A,由于之前检查到instance为空。所以也会执行29行(#2)创建实例。返回。
      4、至此,已经有两个实例被创建了,这不是我们所希望的。 

     怎么解决线程安全问题?

      方法一:同步方法。即在getInstance()方法上加上synchronized关键字。这时单例变成了  

    使用同步方法的单例

      加上synchronized后确实实现了线程的互斥访问getInstance()方法。从而保证了线程安全。但是这样就完美了么?我们看。其实在典型实现里,会导致问题的只是当instance还没有被实例化的时候,多个线程访问#1的代码才会导致问题。而当instance已经实例化完成后。每次调用getInstance(),其实都是直接返回的。即使是多个线程访问,也不会出问题。但给方法加上synchronized后。所有getInstance()的调用都要同步了。其实我们只是在第一次调用的时候要同步。而同步需要消耗性能。这就是问题。

      方法二:双重检查加锁Double-checked locking。
      
    其实经过分析发现,我们只要保证 instance = new SingletonOne(); 是线程互斥访问的就可以保证线程安全了。那把同步方法加以改造,只用synchronized块包裹这一句。就得到了下面的代码:

    复制代码
    1     public static SingletonThree getInstance() {
    2 if (instance == null) { // 1
    3 synchronized (SingletonThree.class) {
    4 instance = new SingletonThree(); // 2
    5 }
    6 }
    7 return instance;
    8 }
    复制代码

      这个方法可行么?分析一下发现是不行的!
      1、线程A和线程B同时进入//1的位置。这时instance是为空的。
      2、线程A进入synchronized块,创建实例,线程B等待。
      3、线程A返回,线程B继续进入synchronized块,创建实例。。。
      4、这时已经有两个实例创建了。 

      为了解决这个问题。我们需要在//2的之前,再加上一次检查instance是否被实例化。(双重检查加锁)接下来,代码变成了这样:

    复制代码
     1     public static SingletonThree getInstance() {
    2 if (instance == null) { // 1
    3 synchronized (SingletonThree.class) {
    4 if (instance == null) {
    5 instance = new SingletonThree(); // 2
    6 }
    7 }
    8 }
    9 return instance;
    10 }
    复制代码

      这样,当线程A返回,线程B进入synchronized块后,会先检查一下instance实例是否被创建,这时实例已经被线程A创建过了。所以线程B不会再创建实例,而是直接返回。貌似!到此为止,这个问题已经被我们完美的解决了。遗憾的是,事实完全不是这样!这个方法在单核和 多核的cpu下都不能保证很好的工作。导致这个方法失败的原因是当前java平台的内存模型。java平台内存模型中有一个叫“无序写”(out-of-order writes)的机制。正是这个机制导致了双重检查加锁方法的失效。这个问题的关键在上面代码上的第5行:instance = new SingletonThree(); 这行其实做了两个事情:1、调用构造方法,创建了一个实例。2、把这个实例赋值给instance这个实例变量。可问题就是,这两步jvm是不保证顺序的。也就是说。可能在调用构造方法之前,instance已经被设置为非空了。下面我们看一下出问题的过程:
      1、线程A进入getInstance()方法。
      2、因为此时instance为空,所以线程A进入synchronized块。
      3、线程A执行 instance = new SingletonThree(); 把实例变量instance设置成了非空。(注意,实在调用构造方法之前。)
      4、线程A退出,线程B进入。
      5、线程B检查instance是否为空,此时不为空(第三步的时候被线程A设置成了非空)。线程B返回instance的引用。(问题出现了,这时instance的引用并不是SingletonThree的实例,因为没有调用构造方法。) 
      6、线程B退出,线程A进入。
      7、线程A继续调用构造方法,完成instance的初始化,再返回。 

      好吧,继续努力,解决由“无序写”带来的问题。

    复制代码
     1     public static SingletonThree getInstance() {
    2 if (instance == null) {
    3 synchronized (SingletonThree.class) { // 1
    4 SingletonThree temp = instance; // 2
    5 if (temp == null) {
    6 synchronized (SingletonThree.class) { // 3
    7 temp = new SingletonThree(); // 4
    8 }
    9 instance = temp; // 5
    10 }
    11 }
    12 }
    13 return instance;
    14 }
    复制代码

      解释一下执行步骤。
      1、线程A进入getInstance()方法。
      2、因为instance是空的 ,所以线程A进入位置//1的第一个synchronized块。
      3、线程A执行位置//2的代码,把instance赋值给本地变量temp。instance为空,所以temp也为空。 
      4、因为temp为空,所以线程A进入位置//3的第二个synchronized块。
      5、线程A执行位置//4的代码,把temp设置成非空,但还没有调用构造方法!(“无序写”问题) 
      6、线程A阻塞,线程B进入getInstance()方法。
      7、因为instance为空,所以线程B试图进入第一个synchronized块。但由于线程A已经在里面了。所以无法进入。线程B阻塞。
      8、线程A激活,继续执行位置//4的代码。调用构造方法。生成实例。
      9、将temp的实例引用赋值给instance。退出两个synchronized块。返回实例。
      10、线程B激活,进入第一个synchronized块。
      11、线程B执行位置//2的代码,把instance实例赋值给temp本地变量。
      12、线程B判断本地变量temp不为空,所以跳过if块。返回instance实例。

      好吧,问题终于解决了,线程安全了。但是我们的代码由最初的3行代码变成了现在的一大坨~。于是又有了下面的方法。

      方法三:预先初始化static变量。

    复制代码
     1 /**
    2 * 预先初始化static变量 的单例模式 非Lazy 线程安全
    3 * 优点:
    4 * 1、线程安全
    5 * 缺点:
    6 * 1、非懒加载,如果构造的单例很大,构造完又迟迟不使用,会导致资源浪费。
    7 *
    8 * @author laichendong
    9 * @since 2011-12-5
    10 */
    11 public class SingletonFour {
    12
    13 /** 单例变量 ,static的,在类加载时进行初始化一次,保证线程安全 */
    14 private static SingletonFour instance = new SingletonFour();
    15
    16 /**
    17 * 私有化的构造方法,保证外部的类不能通过构造器来实例化。
    18 */
    19 private SingletonFour() {
    20
    21 }
    22
    23 /**
    24 * 获取单例对象实例
    25 *
    26 * @return 单例对象
    27 */
    28 public static SingletonFour getInstance() {
    29 return instance;
    30 }
    31
    32 }
    复制代码

      看到这个方法,世界又变得清净了。由于java的机制,static的成员变量只在类加载的时候初始化一次,且类加载是线程安全的。所以这个方法实现的单例是线程安全的。但是这个方法却牺牲了Lazy的特性。单例类加载的时候就实例化了。如注释所述:非懒加载,如果构造的单例很大,构造完又迟迟不使用,会导致资源浪费。

      那到底有没有完美的办法?懒加载,线程安全,代码简单。

      方法四:使用内部类。

    复制代码
     1 /**
    2 * 基于内部类的单例模式 Lazy 线程安全
    3 * 优点:
    4 * 1、线程安全
    5 * 2、lazy
    6 * 缺点:
    7 * 1、待发现
    8 *
    9 * @author laichendong
    10 * @since 2011-12-5
    11 */
    12 public class SingletonFive {
    13
    14 /**
    15 * 内部类,用于实现lzay机制
    16 */
    17 private static class SingletonHolder{
    18 /** 单例变量 */
    19 private static SingletonFive instance = new SingletonFive();
    20 }
    21
    22 /**
    23 * 私有化的构造方法,保证外部的类不能通过构造器来实例化。
    24 */
    25 private SingletonFive() {
    26
    27 }
    28
    29 /**
    30 * 获取单例对象实例
    31 *
    32 * @return 单例对象
    33 */
    34 public static SingletonFive getInstance() {
    35 return SingletonHolder.instance;
    36 }
    37
    38 }
    复制代码

      解释一下,因为java机制规定,内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载(实现了lazy),而且其加载过程是线程安全的(实现线程安全)。内部类加载的时候实例化一次instance。

      最后,总结一下:
      1、如果单例对象不大,允许非懒加载,可以使用方法三。
      2、如果需要懒加载,且允许一部分性能损耗,可以使用方法一。(官方说目前高版本的synchronized已经比较快了)
      3、如果需要懒加载,且不怕麻烦,可以使用方法二。
      4、如果需要懒加载,没有且!推荐使用方法四。 

     

  • 相关阅读:
    C# 文件上传下载
    javaScript的三种储存方式
    XDocument常用属性
    NoSQL
    SQLserver 的分页存储过程
    Redis Wendows安装步骤
    C#和JS获取时间
    api跨域
    以太坊开发DApp入门教程——区块链投票系统(一)
    android和java以太坊开发区块链应用使用web3j类库
  • 原文地址:https://www.cnblogs.com/rocky-AGE-24/p/5131978.html
Copyright © 2011-2022 走看看