设计模式系列之(1):单例模式


1 介绍:
    单例模式是非常简单、非常基础的一种创建型设计模式。
单例模式是在Java开发(不论是android开发,还是Java服务器端开发)中经常使用的一个设计模式。由于在Java服务器端开发中经常使用,所以单例模式的实现还涉及到了Java并发编程。在java面试中,常常被要求使用java实现单例模式,因此有必要熟练掌握。本文是自己学习java单例模式的一个总结,参考了网上的很多资料,大部分内容不是原创。在这里要向参考文献的作者表示感谢。
    本文组织结构如下: 2 介绍单例模式的java实现到底有多少种;3-9分别介绍这些实现方式; 10 提出一些进一步的问题; 11 总结本文。


2 到底有多少种实现?
    "【深入】java 单例模式"一文列出了5种实现方式,一种为GOF的、线程不安全的实现方式,剩下4种是线程安全(thread-safe)的实现方式,分别为:同步方法(synchronized function)方式、双重检查加锁(DCL,double-checked  locking)方式、急切加载方式 和内部类方式。

    "Java:单例模式的七种写法"一文列出了7种实现方式:线程不安全方式、同步方法方式、急切加载方式(及其变种)、静态内部类方式、枚举方式和双重校验锁方式。 但急切加载方式及其变种应该统一看做是一种实现。

    所以本文的结论就是: java单例模式一共有下面将要介绍的5种实现方式。

 



4. 第一种: 线程不安全方式(GOF方式经典方式) ( 懒加载)

这是GOF和HeadFirstDesignPattern中介绍的最经典的一种方式。这种方式在单线程情况下可以实现懒加载,但在多线程情况下会出问题。

 1 public class Creation_singleton {        
 2   // private static instance, which is default null        
 3   private static Creation_singleton instance;
 4   // private constructor        
 5   private Creation_singleton () {
 6   }                
 7   // public get instance function        
 8   public static Creation_singleton getInstance ( ) {
 9     if ( instance == null ) {         
10       instance = new Creation_singleton();        
11     }          
12     return instance;       
13   }  
14 }


5. 第二种:简单加锁方式 ( synchronized )

由第一种而来,很自然会想到,保证线程安全的方式可以是使用synchronized关键字。这种方式是线程安全,又是懒加载。但使用synchronized 会降低性能。

 1 public class Creation_singleton {        
 2   private static Creation_singleton instance;            
 3   private Creation_singleton () {        }               
 4   public static Synchronized Creation_singleton getInstance ( ) { 
 5     if ( instance == null ) {              
 6       instance = new Creation_singleton();        
 7     }          
 8     return instance;    
 9   }  
10 }

我们必须避免使用synchronization,因为它很慢。----上述论断在一定程度上已经不是事实了,因为在现代JVM上,如果是无竞争的同步的话,synchronized并不会很慢了。但是当在多线程环境中,多个线程同时竞争getInstance方法时,还是可能会导致性能下降。

之所以说上述论断不是事实,是与显式锁进行比较得出的结论。内置锁一度被认为比Lock显式锁的性能低很多,但随着JVM的优化,synchronized关键字的性能已经不比Lock低很多了。


 
6. 急切加载方式 ( 变种 )

 1 class Singleton {         
 2   //私有,静态的类自身实例    
 3   private static Singleton instance = new Singleton();         
 4   //私有的构造子(构造器,构造函数,构造方法)    
 5   private Singleton(){}         
 6   //公开,静态的工厂方法     
 7   public static Singleton getInstance() {
 8         return instance;    
 9   }
10 }

 这种方式有一种变种,就是使用静态初始化块来初始化单例。

 有人说这种方式有个缺点:其实跟全局变量是一样的: 不管该实例是不是到了实际被使用的时刻,也不管应用程序最终是不是使用该单例,在类加载 时都会创建该单例,所以叫“急切创建”, 但是这样一来,单例模式的延迟创建的优点就没有了。 所以该版本与全局变量的方式并没有区别(?yes) static 成员变量只有在类加载的时候初始化一次。类加载是线程安全的。 所以该方法实现的单例是线程安全的。 (static的全局变量也是线程安全的)

 但是jvm的类加载也是懒加载的,并不是一开始启动jvm的时候就全部加载所有类。加载这个类,跟创建这个类的实例,二者的时机,在大部分时候,应该是同时的,也就是说,应该不存在很多跟创建这个类的实例无关的、需要加载这个类的情况。正是由于这个原因,这种方式,在NTS代码中使用的很多。


7. 双重检查加锁(dcl)方式
在生产环境中可以看到如下代码,是双重检查加锁的实现。双重检查加锁是简单加锁方式的一种“自作聪明”的改进,其实这种方式在多线程环境下会失败。

 1    private static SocketFactory instance  = null;
 2 
 3    private static SocketFactory getInstance()
 4    {
 5       if (instance == null) 
 6       {
 7          synchronized (SocketFactory.class)
 8          {
 9             if (instance == null)
10             {
11                instance = new SocketFactory();
12             }
13          }
14       }
15       return instance;
16    }

双重检查加锁方式也可以正确地被实现,前提是对上述双重检查加锁方式做出一定的改进。这也让双重检查加锁方式成为了最复杂的实现方式。因此这种方式是不被推荐的。 双重检查加锁方式的正确实现由三种:volatile方式、 final方式、 temp instance方式。因为temp instance 方式非常的繁琐,在本文中就不再做介绍了,有兴趣的读者可以去参考文献中找答案。

7.1. 使用DCL+volatile

在Java5中,DCL实际上是可行的,如果你给instance域加上volatile修饰符。例如,如果我们需要给getInstance()方法传递一个database connection的话,那么以下代码是可行的:

 1 public class MyFactory {
 2   private static volatile MyFactory instance;
 3 
 4   public static MyFactory getInstance(Connection conn)
 5        throws IOException {
 6     if (instance == null) {
 7       synchronized (MyFactory.class) {
 8         if (instance == null)
 9           instance = new MyFactory(conn);
10       }
11     }
12     return instance;  
13   }
14 
15   private MyFactory(Connection conn) throws IOException {
16     // init factory using the database connection passed in
17   }
18 }

但是需要注意的是,上述代码仅仅在Java5及其以上版本中才可以,因为Java5对volatiel关键字的语义进行了修正。(Java4及其以前版本中Volatile关键字的语义都不是完全正确的)。当使用Java5获取一个volatile变量时,获取行为具有synchronization同步语义。换句话说,Java5保证了如下的Happen-before规则:对volatile变量的未同步的读操作必须在写操作之后才能发生,从而读线程将会看到MyFactory对象的所有域的正确值。

7.2 把instance域声明为final

从java 5 之后才有的新特性之一是,final 域的语义有了新的变化。这些域的值是在构造函数中被赋值的,JVM会确保这些值在这个对象引用自己之前被提交到主内存。

换句话说,如果其他线程可以看到这个对象,那么它们永远不可能看到这个对象的final域的未经初始化的值。所以在这种情况下,我们将不需要把这个instance的引用声明为volatile。

(问题:是否需要把所有域都声明为final? )


8. 静态嵌套类方式

1 public class Creation_singleton_innerClass {          
2   private static class SingletonHolder{         
3     private static Creation_singleton_innerClass instance = new Creation_singleton_innerClass();     
4     }          
5   private Creation_singleton_innerClass() {              }          
6   public static Creation_singleton_innerClass getInstance() {         
7     return SingletonHolder.instance;     
8   } 
9 }    



 该方式与急切创建方式有些类似。 java机制规定,内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载(实现了lazy), 说明内部类的加载时机跟外部类的加载时机不同。 而且其加载过程是线程安全的(实现线程安全)。内部类加载的时候实例化一次instance,以后不会再初始化。
 该实现方式可以有一些小变化:instance可以加一个final修饰;静态嵌套类可以改为内部类。

9. 枚举方式
 

1 enum Singleton_enum {    
2   INSTANCE;
3   void method () {
4   }
5 }



该方式是《effective java》中推荐的方法。 枚举类型是在java1.5中引入的,enum可以看做是一种特殊的class,除了不能继承。INSTANCE是Singleton_enum类的实例。
 
上述代码会被JVM编译为:

1 final class MySingleton {     
2   final static MySingleton INSTANCE = new MySingleton();      
3   void method () {     }
4 }

虽然enum的域是编译时常量,但他们是对应enum类型的实例,他们仅在对应enum类型首次被引用的时候被生成。当代码首次运行到访问INSTANCE处时,类MyStingleton才会被JVM装载和初始化。该装载和初始化过程只初始化static域一次。

10 进一步问题:

我在一次面试中,被问到如下问题:单例模式与static方法的区别是什么? 当时没回答上来,现在把答案整理如下:

10.1 单例模式与 static方法的简单比较
static 方法无法实现单例;

如果类与其他类的交互比较复杂,容易造成一些跟初始化有关的、很难调试的bug;
static是急切初始化。
static方法最大的不同就是不能作为参数传递给别的方法。单例可以把这个单例object作为参数传递给其他方法,并当做普通对象来使用。
一个静态类只允许静态方法。

11 参考文献:
http://www.cnblogs.com/coffee/archive/2011/12/05/inside-java-singleton.html
http://www.blogjava.net/kenzhh/archive/2015/04/06/357824.html
http://www.raychase.net/257
https://en.wikipedia.org/wiki/Singleton_pattern

原文地址:https://www.cnblogs.com/hzg1981/p/4993845.html