zoukankan      html  css  js  c++  java
  • 单例模式

    转载:http://devbean.blog.51cto.com/448512/203501/

    Java设计模式(十) 你真的用对单例模式了吗?     (nice)

    为什么我墙裂建议大家使用枚举来实现单例。 (包含了7种单例实现的原理,源码理解)

    深度解析单例与序列化之间的爱恨情仇~

    自己动手实现牛逼的单例模式

    单例模式

    在GoF的23种设计模式中,单例模式是比较简单的一种。然而,有时候越是简单的东西越容易出现问题。下面就单例设计模式详细的探讨一下。
     
    所谓单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在。就像是Java Web中的application,也就是提供了一个全局变量,用处相当广泛,比如保存全局数据,实现全局性的操作等。
     

    0.单例需要关注的点

    线程安全(必须)

    延迟加载

    序列化

     

    1. 最简单的实现(饿汉)

    首先,能够想到的最简单的实现是,把类的构造函数写成private的,从而保证别的类不能实例化此类,然后在类中提供一个静态的实例并能够返回给使用者。这样,使用者就可以通过这个引用使用到这个类的实例了。
     
    public class SingletonClass {               //饿汉

      private static final SingletonClass instance = new SingletonClass(); 
        
      public static SingletonClass getInstance() { 
        return instance; 
      } 
        
      private SingletonClass() { 
         
      } 
        
    }
     
    如上例,外部使用者如果需要使用SingletonClass的实例,只能通过getInstance()方法,并且它的构造方法是private的,这样就保证了只能有一个对象存在。
     

    2. 性能优化——lazy loaded(懒汉)

    上面的代码虽然简单,但是有一个问题——无论这个类是否被使用,都会创建一个instance对象。如果这个创建过程很耗时,比如需要连接10000次数据库(夸张了…:-)),并且这个类还并不一定会被使用,那么这个创建过程就是无用的。怎么办呢?
     
    为了解决这个问题,我们想到了新的解决方案:
     
    public class SingletonClass {         //懒汉

      private static SingletonClass instance = null; 
        
      public static SingletonClass getInstance() { 
        if(instance == null) { 
          instance = new SingletonClass(); 
        } 
        return instance; 
      } 
        
      private SingletonClass() { 
         
      } 
        
    }
     
    代码的变化有两处——首先,把instance初始化为null,直到第一次使用的时候通过判断是否为null来创建对象。因为创建过程不在声明处,所以那个final的修饰必须去掉。
     
    我们来想象一下这个过程。要使用SingletonClass,调用getInstance()方法。第一次的时候发现instance是null,然后就新建一个对象,返回出去;第二次再使用的时候,因为这个instance是static的,所以已经不是null了,因此不会再创建对象,直接将其返回。
     
    这个过程就成为lazy loaded,也就是迟加载——直到使用的时候才进行加载。
     

    3. 同步

    上面的代码很清楚,也很简单。然而就像那句名言:“80%的错误都是由20%代码优化引起的”。单线程下,这段代码没有什么问题,可是如果是多线程,麻烦就来了。我们来分析一下:
     
    线程A希望使用SingletonClass,调用getInstance()方法。因为是第一次调用,A就发现instance是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用SingletonClass,调用getInstance()方法,同样检测到instance是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个SingletonClass的对象——单例失败!
     
    解决的方法也很简单,那就是加锁:
     
    public class SingletonClass { 

      private static SingletonClass instance = null; 
        
      public synchronized static SingletonClass getInstance() { 
        if(instance == null) { 
          instance = new SingletonClass(); 
        } 
        return instance; 
      } 
        
      private SingletonClass() { 
         
      } 
        
    }
     
    是要getInstance()加上同步锁,一个线程必须等待另外一个线程创建完成后才能使用这个方法,这就保证了单例的唯一性。
     

    4. 又是性能

    上面的代码又是很清楚很简单的,然而,简单的东西往往不够理想。这段代码毫无疑问存在性能的问题——synchronized修饰的同步块可是要比一般的代码段慢上几倍的!如果存在很多次getInstance()的调用,那性能问题就不得不考虑了!
     
    让我们来分析一下,究竟是整个方法都必须加锁,还是仅仅其中某一句加锁就足够了?我们为什么要加锁呢?分析一下出现lazy loaded的那种情形的原因。原因就是检测null的操作和创建对象的操作分离了。如果这两个操作能够原子地进行,那么单例就已经保证了。于是,我们开始修改代码:
     
    public class SingletonClass { 

      private static SingletonClass instance = null; 
        
      public static SingletonClass getInstance() { 
        synchronized (SingletonClass.class) { 
          if(instance == null) { 
            instance = new SingletonClass(); 
          } 
        }     
        return instance; 
      } 
        
      private SingletonClass() { 
         
      } 
        
    }
     
    首先去掉getInstance()的同步操作,然后把同步锁加载if语句上。但是这样的修改起不到任何作用:因为每次调用getInstance()的时候必然要同步,性能问题还是存在。如果……如果我们事先判断一下是不是为null再去同步呢?
     
    public class SingletonClass { 

      private static SingletonClass instance = null; 

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

      private SingletonClass() { 

      } 

    }
     
    还有问题吗?首先判断instance是不是为null,如果为null,加锁初始化;如果不为null,直接返回instance。
     
    这就是double-checked locking设计实现单例模式。到此为止,一切都很完美。我们用一种很聪明的方式实现了单例模式。
     

    5. 从源头检查

    下面我们开始说编译原理。所谓编译,就是把源代码“翻译”成目标代码——大多数是指机器代码——的过程。针对Java,它的目标代码不是本地机器代码,而是虚拟机代码。编译原理里面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。这个过程成为reorder。
     
    要知道,JVM只是一个标准,并不是实现。JVM中并没有规定有关编译器优化的内容,也就是说,JVM实现可以自由的进行编译器优化。
     
    下面来想一下,创建一个变量需要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。
     
    下面我们来考虑这么一种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了!
     
    于是,我们想到了下面的代码:
     
    public class SingletonClass { 

      private static SingletonClass instance = null; 

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

      private SingletonClass() { 

      } 
        
    }
     
    我们在第一个同步块里面创建一个临时变量,然后使用这个临时变量进行对象的创建,并且在最后把instance指针临时变量的内存空间。写出这种代码基于以下思想,即synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。因此,在外部的同步块里面对临时变量sc进行操作并不影响instance,所以外部类在instance=sc;之前检测instance的时候,结果instance依然是null。
     
    不过,这种想法完全是错误的!同步块的释放保证在此之前——也就是同步块里面——的操作必须完成,但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器完全可以把instance=sc;这句移到内部同步块里面执行。这样,程序又是错误的了!
     

    6. 解决方案(double-check)

    说了这么多,难道单例没有办法在Java中实现吗?其实不然!
     
    在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了。
     
    public class SingletonClass { 

      private volatile static SingletonClass instance = null; 

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

      private SingletonClass() { 

      } 
        
    }

    7. 静态内部类

    然而,这只是JDK1.5之后的Java的解决方案,那之前版本呢?其实,还有另外的一种解决方案,并不会受到Java版本的影响:
     
    public class SingletonClass { 
        
      private static class SingletonClassInstance { 
        private static final SingletonClass instance = new SingletonClass(); 
      } 

      public static SingletonClass getInstance() { 
        return SingletonClassInstance.instance; 
      } 

      private SingletonClass() { 

      } 
        
    }
     
    在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。
     
    由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。
     
    至此,我们完整的了解了单例模式在Java语言中的时候,提出了两种解决方案。个人偏向于第二种,并且Effiective Java也推荐的这种方式。

    double-check问题探讨

    没有volatile的double-check看似没问题,其实有问题。这个问题由指令重排序引起。

    指令重排序是为了优化指令,提高程序运行效率。指令重排序包括编译器重排序和运行时重排序。JVM规范规定,指令重排序可以在不影响单线程程序执行结果前提下进行。例如 instance = new Singleton() 可分解为如下伪代码:

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

    但是经过重排序后如下:

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

    将第2步和第3步调换顺序,在单线程情况下不会影响程序执行的结果,但是在多线程情况下就不一样了。线程A执行了instance = memory(这对另一个线程B来说是可见的),此时线程B执行外层 if (instance == null),发现instance不为空,随即返回,但是得到的却是未被完全初始化的实例,在使用的时候必定会有风险,这正是双重检查锁定的问题所在!

    这里使用了volatile的内存可见性与禁止指令重排。

    枚举实现单例(可读性差,不推荐)

    public enum Singleton {  
        INSTANCE;  
        public void whateverMethod() {  
        }  
    } 

    枚举可解决线程安全问题

    上面提到过。使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?

    其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。

    那么,“底层”到底指的是什么?

    这就要说到关于枚举的实现了。这部分内容可以参考我的另外一篇博文《深度分析Java的枚举类型—-枚举的线程安全性及序列化问题》,这里我简单说明一下:

    定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。

    通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。

    而且,枚举中的各个枚举项同事通过static来定义的。如:

    public enum T {
        SPRING,SUMMER,AUTUMN,WINTER;
    }

    反编译后代码为:

    public final class T extends Enum
    {
        //省略部分内容
        public static final T SPRING;
        public static final T SUMMER;
        public static final T AUTUMN;
        public static final T WINTER;
        private static final T ENUM$VALUES[];
        static
        {
            SPRING = new T("SPRING", 0);
            SUMMER = new T("SUMMER", 1);
            AUTUMN = new T("AUTUMN", 2);
            WINTER = new T("WINTER", 3);
            ENUM$VALUES = (new T[] {
                SPRING, SUMMER, AUTUMN, WINTER
            });
        }
    }

    了解JVM的类加载机制的朋友应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,我们在深度分析Java的ClassLoader机制(源码级别)中介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。

    也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。

    所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

    枚举可避免反序列化破坏单例

    前面我们提到过,使用“双重校验锁”实现的单例其实是存在一定问题的,就是这种单例有可能被序列化锁破坏,关于这种破坏及解决办法,参看单例与序列化的那些事儿,这里不做更加详细的说明了。

    那么,对于序列化这件事情,为什么枚举又有先天的优势了呢?答案可以在Java Object Serialization Specification 中找到答案。其中专门对枚举的序列化做了如下规定:

    大概意思就是:在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.EnumvalueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObjectreadObject等方法。

    普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。

    但是,枚举的反序列化并不是通过反射实现的。Java中有明确规定,枚举的序列化和反序列化是有特殊定制的。这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。所以,也就不会发生由于反序列化导致的单例破坏问题。这部分内容在《深度分析Java的枚举类型—-枚举的线程安全性及序列化问题》中也有更加详细的介绍,还展示了部分代码,感兴趣的朋友可以前往阅读。

    静态内部类已有问题的解决(*****)

    反射下,单例结构的保证

    在反射的作用下,静态内部类的单例结构是会被破坏的。测试代码如下所示:

    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    import singleton.LazySingleton2;
    
    public class LazySingleton2Test {
        public static void main(String[] args) {
            //创建第一个实例
            LazySingleton2 instance1 = LazySingleton2.getInstance();
        
            //通过反射创建第二个实例
            LazySingleton2 instance2 = null;
            try {
                Class<LazySingleton2> clazz = LazySingleton2.class;
                Constructor<LazySingleton2> cons = clazz.getDeclaredConstructor();
                cons.setAccessible(true);
                instance2 = cons.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            //检查两个实例的hash值
            System.out.println("Instance 1 hash:" + instance1.hashCode());
            System.out.println("Instance 2 hash:" + instance2.hashCode());
        }
    }

    输出如下:

    Instance 1 hash:1694819250
    Instance 2 hash:1365202186

    根据哈希值可以看出,反射破坏了单例的特性。解决方案:

    public class LazySingleton3 {
    
        private static boolean initialized = false;
    
        private LazySingleton3() {
            synchronized (LazySingleton3.class) {
                if (initialized == false) {
                    initialized = !initialized;
                } else {
                    throw new RuntimeException("单例已被破坏");
                }
            }
        }
    
        static class SingletonHolder {
            private static final LazySingleton3 instance = new LazySingleton3();
        }
    
        public static LazySingleton3 getInstance() {
            return SingletonHolder.instance;
        }
    }

    此时再运行一次测试类,出现如下提示:

    java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at test.LazySingleton3Test.main(LazySingleton3Test.java:21)
    Caused by: java.lang.RuntimeException: 单例已被破坏
        at singleton.LazySingleton3.<init>(LazySingleton3.java:12)
        ... 5 more
    Instance 1 hash:359023572

    这里就保证了,反射无法破坏其单例特性。

    序列化下,单例结构的保证

    在分布式系统中,有些情况下你需要在单例类中实现 Serializable 接口。这样你可以在文件系统中存储它的状态并且在稍后的某一时间点取出。

    先将

    public class LazySingleton3

     变为

    public class LazySingleton3 implements Serializable

    上测试类如下:

    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.ObjectInput;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutput;
    import java.io.ObjectOutputStream;
    
    import singleton.LazySingleton3;
    
    public class LazySingleton3Test {
        public static void main(String[] args) {
            try {
                LazySingleton3 instance1 = LazySingleton3.getInstance();
                ObjectOutput out = null;
    
                out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
                out.writeObject(instance1);
                out.close();
    
                //deserialize from file to object
                ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
                LazySingleton3 instance2 = (LazySingleton3) in.readObject();
                in.close();
    
                System.out.println("instance1 hashCode=" + instance1.hashCode());
                System.out.println("instance2 hashCode=" + instance2.hashCode());
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    输出如下:

    instance1 hashCode=2051450519
    instance2 hashCode=1510067370

    显然,我们又看到了两个实例类。为了避免此问题,我们需要提供 readResolve() 方法的实现。readResolve()代替了从流中读取对象。这就确保了在序列化和反序列化的过程中没人可以创建新的实例。代码如下:

    import java.io.Serializable;
    
    public class LazySingleton4 implements Serializable {
    
        private static boolean initialized = false;
    
        private LazySingleton4() {
            synchronized (LazySingleton4.class) {
                if (initialized == false) {
                    initialized = !initialized;
                } else {
                    throw new RuntimeException("单例已被破坏");
                }
            }
        }
    
        static class SingletonHolder {
            private static final LazySingleton4 instance = new LazySingleton4();
        }
    
        public static LazySingleton4 getInstance() {
            return SingletonHolder.instance;
        }
        
        private Object readResolve() {
            return getInstance();
        }
    }

    此时,在运行测试类,输出如下:

    instance1 hashCode=2051450519
    instance2 hashCode=2051450519

    这表示此时已能保证序列化和反序列化的对象是一致的。

  • 相关阅读:
    ElasticSearch已经配置好ik分词和mmseg分词(转)
    Java:Cookie实现记住用户名、密码
    (转)10大H5前端框架
    msysGit在GitHub代码托管
    mongodb 语句和SQL语句对应(SQL to Aggregation Mapping Chart)
    centos 7 安装redis
    mac下idea卡顿问题解决
    在linux系统中,使用tomcat的shutdown.sh脚本停止应用,但是进程还在的解决办法
    centOS 7.4 安装配置jdk1.8
    CentOS6 在线安装PostgreSQL10
  • 原文地址:https://www.cnblogs.com/fanguangdexiaoyuer/p/5787192.html
Copyright © 2011-2022 走看看