单例设计模式(这一篇足够了)

  单例模式真是一个老掉牙的问题了,不过我今天是要说些里面更深点的知识,闲话少说,直接来代码

  1、饿汉式

   相信这种写法大家都知道,一开始接触单例的时候,大家应该都是用的这种方法:

package com.hd.single;

public class Singleton {

    private Singleton(){}
    private static Singleton instance = new Singleton();

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

  这种方式优点就是线程安全, 缺点也很明显,就是类加载的时候,就已实例化该对象了,后面有可能用不到这个实例对象,这样就会造成空间浪费。因此就有了懒加载方式。

  2、懒汉式

  1)懒汉式L1

package com.hd.single;

public class Singleton2 {

    private Singleton2(){}
    private static Singleton2 instance;

    public static Singleton2 getInstance(){
        if(instance == null)        //1
            instance = new Singleton2();  //2
        return instance;
    }

}

  这种懒汉式的优点和缺点也很明显,优点是按需加载,节省空间, 缺点是线程不安全。简单说就是,有可能线程A执行到“1”处时,阻塞住了,线程B抢到CPU,进来执行并实例化对象,然后线程A醒来后,继续往下执行,这样线程A和B取到的就是不同的对象。因此,又有了线程安全的版本。

package com.hd.single;

public class Singleton2 {

    private Singleton2(){}
    private static Singleton2 instance;

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

  但是加了synchronized 之后会造成线程阻塞,影响性能。于是又提出了双检锁的方式

 1 package com.hd.single;
 2 
 3 public class Singleton2 {
 4 
 5     private Singleton2(){}
 6     private static Singleton2 instance;
 7 
 8     public static Singleton2 getInstance(){
 9         if(instance == null){
10             synchronized (Singleton2.class){
11                 if(instance == null){
12                     instance = new Singleton2();
13                 }
14             }
15         }
16         return instance;
17     }
18 }

  看似双检锁的方式很完美,既解决了线程安全的问题,又兼顾了性能问题: 线程先判断instance变量是否为空,如果不为空,则直接返回。否则进入同步块去实例化对象。但事实这是一个错误的优化!

  重点就是第12行代码(instance = new Singleton2();), 它创建了一个对象。这一行代码可以分解为如下的3行代码:

memory = allocate();         //1:分配对象的内存空间
ctorInstance(memory);        //2:初始化对象
instance = memory;           //3:设置instance指向刚分配的内存地址

  上面2和3这两步在执行的时候,有可能会被重排序(具体指令重排序知识点,可以去网上搜索相关内容,一大堆,我就不详细说了。本质就是jvm为了优化而使用的),2和3重排序之后的执行时序如下:

memory = allocate();         //1:分配对象的内存空间
instance = memory;           //3:设置instance指向刚分配的内存地址
                           //注意此时对象还没有被初始化
ctorInstance(memory);        //2:初始化对象
                                    

  因此如果有线程A执行到3时,此时instance变量确实不为空,然后线程B判断instance不为空后返回,那么这是时程B 取到的就是一个空的对象。显示这样是有问题的,因此为了防止出现这个问题,我们可以使用volatile变量,来禁止指令重排序。

  2)懒汉式 L2(基于volatile的解决方案)

package com.hd.single;

public class Singleton {

    private Singleton(){}
    private volatile static Singleton instance;

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

  我们除了通过volatile的方式来禁止指令重排序,还可以提供另外一种思路:允许2和3重排序,但不允许其它线程“看到”这个重排序。 前面正是因为线程B看到了重排序,发现instance变量不为空,所以才造成其取到空的对象。

  3)懒汉式L3(基于静态内部类的方案)

package com.hd.single;
public class LazySingleton2 {
    private LazySingleton2() {
    }
    static class SingletonHolder {
        private static final LazySingleton2 instance = new LazySingleton2();
    }
    public static LazySingleton2 getInstance() {
        return SingletonHolder.instance;
    }
}

  因为 在加载外部类时,其内部类不会同时被加载。只有调用 getInstance方法的时候,内部类才会去被加载,且只加载一次,不存在并发问题,因此是线程安全的。

  另外,在getInstance()方法中没有使用synchronized关键字,因此没有造成多余的性能损耗。

  本文给出了多个版本的单例模式,供我们在项目中使用。一般用L2,L3就基本够用。

原文地址:https://www.cnblogs.com/xiexin2015/p/9043429.html