• Java中的23种设计模式之——单例模式(1)


    一、什么是单例模式呢?

    简单来说单例模式就是一个类只能构建一个对象的设计模式。

    二、一个简单的单例模式的代码实现

    /**
     * 单例模式
     * @author ouyanxia
     *
     */
    public class SingletonE {
        //饿汉式
        private static SingletonE singleton = new SingletonE();
        
        private SingletonE(){}//私有构造函数
        
        static SingletonE getInstance(){
            return singleton;
        }
    }
    // 懒汉式
    class SingletonL {
        private static SingletonL instance = null;
        private SingletonL(){}
        static SingletonL getInstance(){
            if(instance==null){
                instance = new SingletonL();
            }
            return instance;
        }
    }

    1、想要让一个类只能构造一个对象,自然不能让它随便去做new操作,因此Signleton的构造方法是私有的。

    2、instance是Signleton类的静态成员,也是我们的单例对象。它的初始值可以写成Null,也可以写成new Signleton()。

    3、getInstance是获取单例对象的方法。

    三、如何实现一个线程安全的单例模式?

    为什么说刚才的代码不是线程安全呢?

    假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法:

    因为Instance是空,所以两个线程同时通过了条件判断,开始执行new操作:

    这样一来,显然instance被构建了两次。让我们对代码做一下修改:

    public class Singleton {
        private Singleton() {}  //私有构造函数
       private static Singleton instance = null;  //单例对象
       //静态工厂方法
       public static Singleton getInstance() {
            if (instance == null) {      //双重检测机制
             synchronized (Singleton.class){  //同步锁
               if (instance == null) {     //双重检测机制
                 instance = new Singleton();
                   }
                }
             }
            return instance;
        }
    }

    为什么这样写呢?我们来解释几个关键点:

    1、为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。

    2、进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。

    像这样两次判空的机制叫做双重检测机制

    四、完全线程安全单例模式实现

    假设这样的场景,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法:

    这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到true;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到false。

    真的如此吗?答案是否定的。这里涉及到了JVM编译器的指令重排

    指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:

    memory =allocate();    //1:分配对象的内存空间 

    ctorInstance(memory);  //2:初始化对象 

    instance =memory;     //3:设置instance指向刚分配的内存地址 

    但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:

    memory =allocate();    //1:分配对象的内存空间 

    instance =memory;     //3:设置instance指向刚分配的内存地址 

    ctorInstance(memory);  //2:初始化对象 

    当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行  if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。如下图所示:

    如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。

    public class Singleton {
        private Singleton() {}  //私有构造函数
        private volatile static Singleton instance = null;  //单例对象
        //静态工厂方法
        public static Singleton getInstance() {
              if (instance == null) {      //双重检测机制
             synchronized (Singleton.class){  //同步锁
               if (instance == null) {     //双重检测机制
                 instance = new Singleton();
                    }
                 }
              }
              return instance;
          }
    }

    经过volatile的修饰,当线程A执行instance = new Singleton的时候,JVM执行顺序是什么样?始终保证是下面的顺序:

    memory =allocate();    //1:分配对象的内存空间 

    ctorInstance(memory);  //2:初始化对象 

    instance =memory;     //3:设置instance指向刚分配的内存地址 

    如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。

  • 相关阅读:
    qcow2文件压缩
    raw格式镜像文件压缩并转换为qcow2格式
    centos7 install virt-sysprep
    镜像简介
    QEMU 使用的镜像文件:qcow2 与 raw
    ubuntu14.04中国源
    less css下载及编绎工具
    分布式计算中WebService的替代方案: RPC (XML-RPC | JSON-RPC)
    Asp.net WebServer
    C#取调用堆栈StackTrace
  • 原文地址:https://www.cnblogs.com/ouyanxia/p/8360002.html
Copyright © 2020-2023  润新知