Java常见设计模式之单例模式

     1.何为单例模式?

     单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。单例模式确保一个类只有一个实例,自行提供这个实例并向整个系统提供这个实例。

     2.单例模式的实现方式

     一般来说单例设计模式有三个具体的实现方式,分别为饿汉模式,懒汉模式以及双重锁设计模式。

    下面我们将要分别介绍它们:

 

     1.饿汉的模式

 

     在初始化类的过程中就会完成相关实例的初始化,一般认为这种方式要更加安全些,饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。

 

     Java代码实例如下:

 

    

 

package com.yonyou.test;
 
/**
 * 使用饥汉模式创建一个单例模式
 * @author 小浩
 * @创建日期 2015-4-4
 */
public class Singleton {
//在加载类的时候就初始化Singleton实例
private final static Singleton instance=new Singleton();
/**
* 返回或者创建相关的单例实例
* @return
*/
public static Singleton getInstance(){
      return instance;
}
/**
* 一个私有的构造方法,使外部的对象不能new相关的实例,
* 这个尤其要注意,一定要提供默认的私有化的方法去覆盖
* 默认的构造方法 否则的话,如果用户直接去new一个对象
* 的话,就无法保证单例了~~~
*/
private Singleton(){}
}

 

 

 

 

    2、懒汉模式

     对于饿汉模式而言。这样做的好处是编写简单,但是无法做到延迟创建对象。但是我们很多时候都希望对象可以尽可能地延迟加载,从而减小负载,所以就需要下面的懒汉法。

     懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实 例,则节约内存空间。

     java代码实例如下:

     a、在单线程的情况下:

     这种方法可以实现延时加载,但是有一个致命弱点:线程不安全。如果有两条线程同时调用getSingleton()方法,就有很大可能导致重复创建对象。

    

public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton() {//线程不安全
        if(singleton == null) 
            singleton = new Singleton();
        return singleton;
    }
}

 

  

    b、考虑线程安全的写法

 

   这种写法考虑了线程安全,将对singleton的null判断以及new的部分使用synchronized进行加锁。同时,对singleton对象使用volatile关键字进行限制,保证其对所有线程的可见性,

   并且禁止对其进行指令重排序优化。如此即可从语义上保证这种单例模式写法是线程安全的。注意,这里说的是语义上,实际使用中还是存在小坑的,会在后文写到。

package com.yonyou.test;
 
/**
 * 使用懒汉模式创建一个单例模式
 * @author 小浩
 * @创建日期 2015-4-4
 */
public class Singleton {
private static volatile Singleton instance=null;
/**
* 返回或者创建相关的单例实例
* @return
*/
public static synchronized Singleton getInstance(){//使用同步方法保证线程安全
if(instance==null){
instance=new Singleton();
}
return instance;
}
/**
* 一个私有的构造方法,使外部的对象不能new相关的实例,
* 这个尤其要注意,一定要提供默认的私有化的方法去覆盖
* 默认的构造方法 否则的话,如果用户直接去new一个对象
* 的话,就无法保证单例了~~~
*/
private Singleton(){}
}

 

此外还有一个比较给力的实现单例的方式,推荐大家使用这种方式:

Google公司的工程师Bob Lee写的新的懒汉单例模式,这里单例模式是在内部类的基础上实现的,
相应的线程安全是由JVM静态初始化器来完成。

 

  没有一种延时加载,并且能保证线程安全的简单写法呢?我们可以把Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,

 并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的:

package com.yonyou.test;
 
/**
 * 使用懒汉模式创建一个单例模式
 * @author 小浩
 * @创建日期 2015-4-4
 */
public class Singleton {    
	 
/**
* 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
* 没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。
*/
   static class SingletonHolder {     
   /**
    *  静态初始化器,由JVM来保证线程安全
    */
     static Singleton instance = new Singleton();    
     
  }    
  
   
  public static Singleton getInstance() {    
        return SingletonHolder.instance;    
  }
   
  /**
  * 一个私有的构造方法,使外部的对象不能new相关的实例,
  * 这个尤其要注意,一定要提供默认的私有化的方法去覆盖
  * 默认的构造方法 否则的话,如果用户直接去new一个对象
  * 的话,就无法保证单例了~~~
  */
private Singleton(){}
} 

 

  

 

     4.双重锁的模式 

  

      咋一看之前的懒汉模式感觉还可以,已经基本实现了单例的设计模式,但是如果我们仔细推敲的话,我们就会发现问题所在。

      在代码

      public static synchronized Singleton getInstance(){//使用同步方法保证线程安全

      这里我们看到,每次获取单例的时候都会加入同步锁,这样的话势必会造成访问性能的问题。

  虽然上面这种写法是可以正确运行的,但是其效率低下,还是无法实际应用。因为每次调用getSingleton()方法,都必须在synchronized

      这里进行排队,而真正遇到需要new的情况是非常少的

      为了更好的解决这个问题,我们可以修改加入同步锁的位置,请看下面的代码:

 

package com.yonyou.test;
  
/**
 * 使用懒汉模式创建一个单例模式
 * @author 小浩
 * @创建日期 2015-4-4
 */
public class Singleton {
private static Singleton instance=null;
/**
* 返回或者创建相关的单例实例
* @return
*/
public static Singleton getInstance(){
	if(instance==null)
	{	
    //加入同步代码块,这样可以有效的处理并发下访问问题
    synchronized(Singleton.class){
            if(instance==null){
            instance=new Singleton();
        }
      }
	}
return instance;
}
/**
* 一个私有的构造方法,使外部的对象不能new相关的实例,
* 这个尤其要注意,一定要提供默认的私有化的方法去覆盖
* 默认的构造方法 否则的话,如果用户直接去new一个对象
* 的话,就无法保证单例了~~~
*/
private Singleton(){}
}

 

     双重锁的主要目的是解决每次获取实例的时候都需要进行线程等待,只有在第一次实例化时,才启用同步机制,提高了性能。 

     这个方法表面上看起来很完美,你只需要付出一次同步块的开销,但它依然有问题。除非你声明instance变量时使用了volatile关键字。没有volatile修饰符,可能出现Java中的另一个线程看到个初始化了一半的instance的情况,但使用了volatile变量后,就能保证先行发生关系(happens-before relationship)。对于volatile变量_instance,在Java 5之前不是这样,所以在这之前使用双重检查锁有问题。现在,有了先行发生的保障(happens-before guarantee),你可以安全地假设其会工作良好。另外,这不是创建线程安全的单例模式的最好方法,你可以使用枚举实现单例模式,这种方法在实例创建时提供了内置的线程安全。另一种方法是使用静态持有者模式(static holder pattern),这种方式就是之前的谷歌工程师给出的方法,另外如果性能不是要求很高的话,可以使用饿汉模式,饿汉模式相对来说比较安全一些。

加入volatile的升级版双重锁。

package com.yonyou.test;
  
/**
 * 使用懒汉模式创建一个单例模式
 * @author 小浩
 * @创建日期 2015-4-4
 */
public class Singleton{
//注意这里面的volatile 这个关键字,它的主要作用是写(write)优于读(read),
//这样就可以保证不会读取到初始化一般	
//前提是jdk的版本要不低于jdk5.0	
private volatile static Singleton instance=null;
/**
 * 
* 返回或者创建相关的单例实例
* @return
*/
public static Singleton getInstance(){
	if(instance==null)
	{	
    //加入同步代码块,这样可以有效的处理并发下访问问题
    synchronized(Singleton.class){
            if(instance==null){
            instance=new Singleton();
        }
      }
	}
return instance;
}
/**
* 一个私有的构造方法,使外部的对象不能new相关的实例,
* 这个尤其要注意,一定要提供默认的私有化的方法去覆盖
* 默认的构造方法 否则的话,如果用户直接去new一个对象
* 的话,就无法保证单例了~~~
*/
private Singleton(){}
}

      提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

   关于volatile关键字的扩展:

 

   那么,这种写法是不是绝对安全呢?前面说了,从语义角度来看,并没有什么问题。但是其实还是有坑。说这个坑之前我们要先来看看volatile这个关键字。其实这个关键字有两层语义。第一层语义相信大家都比较熟悉,就是可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便一提,工作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化。大家知道我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题。

 

   注意,前面反复提到“从语义上讲是没有问题的”,但是很不幸,禁止指令重排优化这条语义直到jdk1.5以后才能正确工作。此前的JDK中即使将变量声明为volatile也无法完全避免重排序所导致的问题。所以,在jdk1.5版本前,双重检查锁形式的单例模式是无法保证线程安全的。

 

     5.枚举模式

 

     但是,上面提到的所有实现方式都有两个共同的缺点:

 

  • 都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。
  • 可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

     下面我么来看一下枚举的实现方式。

 

     这种方法在实例创建时提供了内置的线程安全。

    

package com.yonyou.test;

/**
 * 使用枚举类创建单例模式
 * @author 小浩
 * @创建日期 2015-4-5
 */
public enum Singleton{
		INSTANCE;// 使用枚举类的方法创建一个单例,据说这是目前位置,
		                 //无论是多线程还是,防止反射创建
		                 //相关对象最有用的方法,推荐使用
}

 

测试用例:

package com.jd.test;

/**
 * 使用枚举类创建单例模式
 *
 * Created with IntelliJ IDEA.
 * User: zhanghao10@jd.com
 */
public enum Singleton {
    Instance;

    public void test(){
        System.out.println("zhe");
    }

    Singleton(){

    }

 

package com.jd.biz.finance.demo;

/**
 * Created with IntelliJ IDEA.
 * User: zhanghao10@jd.com
 */
public class Test {

    public static void main(String args[]){
        Singleton.Instance.test();
    }
}

 

  

     单元素的枚举类型已经成为实现Singleton的最佳方法。用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可。使用enum关键字来实现单例模式的好处是这样非常简洁,并且无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。——来自《Effective Java》

 

    

***知识补充(拷贝的)----针对谷歌那个工程师的方法:

 这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。

  1.相应的基础知识

  • 什么是类级内部类?

  简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。

  类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。

  类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。

  类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。

  •  多线程缺省同步锁的知识

  大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:

  1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时

  2.访问final字段时

  3.在创建线程之前创建对象时

  4.线程可以看见它将要处理的对象时

  2.解决方案的思路

  要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性。比如前面的饿汉式实现方式。但是这样一来,不是会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。

  如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。

 

 ****最后需要注意一点,上面除了使用枚举类的方法外,其它的方法都可以通过java的反射机制创建相关的对象,

        即使使用private 重写了相关的构造方法~~

 

 ****为了更好的理解双重锁的弊端,这里我们有必要在简单的讲解一下****

      这里面用到了JVM加载类的知识,对于JVM加载类过程不熟悉的,这里我简单介绍下,熟悉的跳过这段(当然,既然你熟悉就自然会知道双锁的弊端了)。我们知道一个类(class)要被使用必须经过装载,连接,初始化这样的过程。下面先对这三阶段做一个简单的描述,之后会结合一个简单的例子来说明java中类的初始化过程。
     
jvm加载一个类大体分为三个步骤:

  1. 加载阶段:类装载器(Bootstrap ClassLoader 或者用户自定义的ClassLoader) 把编译形成的class文件载入内存,也就是在硬盘上寻找java文件对应的class文件,并将class文件中的二进制数据加载到内存中,将其放在运行期数据区的方法区中去,然后创建类相关的Class对象,这个Class对象封装了我们要使用的在堆区创建一个java.lang.Class对象,用来封装在方法区内的数据结构。
     
  2. 连接阶段:这个阶段分为三个步骤,步骤一:验证,验证什么呢?当然是验证这个class文件里面的二进制数据是否符合java规范咯;步骤二:准备,为该类的静态变量分配内存空间,并将变量赋一个默认值,比如int的默认值为0;在这个阶段,JVM可能还会为一些数据结构分配内存,目的 是提高运行程序的性能,比如说方法表。步骤三:解析,这个阶段就不好解释了,主要是类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用将符号引用转化为直接引用,涉及到指针,这里不做多的解释 。此外这个阶段可以被推迟到初始化之后,当程序运行的过程中真正使用某个符号引用的时候 再去解析它。
     
  3. 初始化阶段:当我们主动调用该类的时候(一定要注意此处),将该类的变量赋于正确的值(这里不要和第二阶段的准备混淆了),举个例子说明下两个区别,比如一个类里有private static int i = 5; 这个静态变量在"准备"阶段会被分配一个内存空间并且被赋予一个默认值0,当道到初始化阶段的时候会将这个变量赋予正确的值即5,了解了吧

         在补充一下初始化的过程:

在Java代码中,一个正确的初始值是通过类变量初始化语句或者静态初始化块或者构造方法给出的。而我们这里所说的主动使用 包括:
1.        创建类的实例
2.        调用类的静态方法
3.        使用类的非常量静态字段
4.        调用Java API中的某些反射方法
5.        初始化某个类的子类
6.        含有main()方法的类启动时
 

     好了,上面大体介绍了jvm类加载过程,回到我们的双锁机制上来分析下问题出在了哪里?假如有两个并发线程a、b,a线程主动调用了静态方法getInstance(),这时开始加载和初始化该类的静态变量,b线程调用getInstance()并等待获得同步锁,当a线程初始化对象过程中,到了第二阶段即连接阶段的准备步骤时,静态变量doubleKey 被赋予了一个默认值,但是这时还没有进行初始化,这时当a线程释放锁后,b线程判断doubleKey != null,则直接返回了一个没有初始化的doubleKey 对象,问题就出现在这里了,b线程拿到的是一个被赋予了默认值但是未初始化的对象,刚刚可以通过锁的检索!

 

 

 

相关连接可以参考:http://www.importnew.com/6461.html

                         http://www.tekbroaden.com/singleton-java.html

  

 

 

 

 

posted on 2015-04-04 10:41  @ 小浩  阅读(902)  评论(0编辑  收藏  举报