小李探花IU

导航

java设计模式之单例模式(Singleton)

   利用元旦小假期,参考了几篇单例模式介绍的文章,然后自己下午对java设计模式中的单例模式做了一下简单的总结,主要是代码介绍。

      单例模式,在实际项目开发中运用广泛,比如数据库连接池,实际上,配置信息类、管理类、控制类、门面类、代理类通常被设计为单例类。像Java的Struts、spring框架,.Net的Spring.Net框架,以及PHP的Zend框架都大量使用单例模式。

  那么,接下来,我将以下面5点来对单例模式作一下介绍:

  1.单例模式的定义

  2.单例模式的特点

  3.为什么要使用单例模式?

  4.单例模式的5种不同写法及其总结

  5.拓展--如何防止Java反射机制对单例类的攻击?

1.单例模式的定义

    单例模式(Singleton)是一种创建型模式,指某个类采用Singleton模式,则在这个类被创建后,只可能产生一个实例供外部访问,并且提供一个全局的访问点。

核心知识点:
 (1)将采用单例模式的类的构造方法私有化(采用private修饰);
 (2)在其内部产生该类的实例化对象,并将其封装成private static类型
 (3)定义一个静态方法返回该类的实例。

2.单例模式的特点

  - 单例类只能有一个实例;
  - 单例类必须自己创建自己的唯一实例;
  - 单例类必须给所有其他对象提供这一实例。

3.为什么要使用单例模式?

  根据单例模式的定义和特点,我们会对单例模式有了初步认识,那么由特点出发,单例模式在项目中的作用就显而易见了。

  (1)控制资源的使用,通过线程同步来控制资源的并发访问
  (2)控制实例产生的数量,到达节约资源的目的;
  (3)作为通信媒介使用,也就是数据共享,他可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程实现通信。
  比如:
  数据库连接池的设计一般采用单例模式,数据库连接是一种数据资源。项目中使用数据库连接池,主要是节省打开或者关闭数据库所引起的效率损耗。当然,使用数据库连接池可以屏蔽不同数据数据库之间的差异,实现系统对数据库的低度耦合,也可以被多个系统同时使用,具有高科复用性,还能方便对数据库连接的管理等。
  实际上,配置信息类、管理类、控制类、门面类、代理类通常被设计为单例类。像Java的Struts、spring框架,.Net的Spring.Net框架,以及PHP的Zend框架都大量使用单例模式。

4.单例模式的5种不同写法及其总结

  单例模式的实现常用的有5种,分别是:

  (1).饿汉式;

  (2).懒汉式(、加同步锁的懒汉式、加双重校验锁的懒汉式、防止指令重排优化的懒汉式);

  (3).登记式单例模式;

  (4)静态内部类单例模式;

  (5).枚举类型的单例模式。

  接下来,我就以代码为主来对各种实现方式介绍一下。

项目工程结构:如图中的红框1中所示。

 

(1).饿汉式

代码清单【1】

 1 package com.lxf.singleton;
 2 
 3 /**
 4 
 5  * 单例类--饿汉模式   线程安全
 6  * @author Administrator
 7  * 
 8  */
 9 public class Singleton 
10 {
11     private static final Singleton INSTANCE = new Singleton();
12     
13     private static boolean flag = true;
14     private Singleton()
15     {
16     }
17     
18     public static Singleton newInstance()
19     {
20         return INSTANCE;
21     }
22 
23 }

  从代码中,我们可以看到,该类的构造函数被定义为private,这样就保证了其他类不能实例化此类,然后该单例类提供了一个静态实例并返回给调用者(向外界提供了调用该类方法的实例)。饿汉模式在类加载的时候就对该实例进行创建,实例在整个程序周期都存在。

  优点:只在类加载的时候创建一次,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题,是线程安全的。
  缺点:在整个程序周期中,即使这个单例没有被用到也会被加载,而且在类加载之后就被创建,内存就被浪费了。
  使用场景:适合单例占用内存比较小,在初始化就被用到的情况。但是,如果单例占用的内存比较大,或者单例只是在某个场景下才会被使用到,使用该模式就不合适了,这时候就要考虑使用“懒汉模式”进行延迟加载。

(2).懒汉式(、加同步锁的懒汉式、加双重校验锁的懒汉式、防止指令重排优化的懒汉式)

2.1--懒汉式(、加同步锁的懒汉式

代码清单【2.1】

 1 package com.lxf.singleton;
 2 
 3 /**
 4  * 懒汉式单例模式   线程不安全
 5  * @author Administrator
 6  *
 7  */
 8 public class Singleton2 
 9 {
10     private static Singleton2 instance = null;
11     
12     private Singleton2(){}
13 
14     /*
15      * 1.未加同步锁
16      */
17     /*
18     public static Singleton2 getInstance()
19     {
20         if(instance == null)
21         {
22             instance = new Singleton2();
23         }
24         return instance;
25     }
26     */
27     
28     /*
29      * 2.加同步锁     线程安全
30      * 上面的懒汉模式并没有考虑多线程的安全问题,在多性格线程可能并发调用它的getInsatance()方法,
31      * 导致创建多个实例,因此需要加锁来解决线程同步问题。
32      */
33     public static synchronized Singleton2 getInstance()
34     {
35         if(instance == null)
36         {
37             instance = new Singleton2();
38         }
39         return instance;
40     }
41
46 }

  懒汉式单例模式是在需要的时候才去创建,如果调用该接口获取实例的时候,发现该实例不存在,就会被创建;如果发现该实例已经存在,就会返回之前已经创建出来的实例。

  但是懒汉模式的单例设计,是线程不安全的,没有考虑线程安全问题。如果你的程序是多线程的,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程的运行结果一样的,而且其他的变量的值也和预期一样的,就是线程安全的。显然,懒汉式单例模式并不是线程安全的,在多线程并发环境下,可能会创建出来多个实例。

  使用场景:适合在项目中使用单例类数量较少,而且占用资源比较多的项目,可以考虑使用懒汉式单例模式。

2.2--加双重校验锁的懒汉式、防止指令重排优化的懒汉式

 

代码清单【2.2】

 

 1 package com.lxf.singleton;
 2 
 3 /**
 4  * 双重校验锁     线程安全
 5  * @author Administrator
 6  * 
 7  */
 8 public class Singleton3
 9 {
10     private static Singleton3 instance = null;
11     //禁止指令重排优化
12     //private static volatile Singleton3 instance = null;
13     private Singleton3(){}
14     
15     public static Singleton3 getInstance()
16     {
17         if(null == instance)
18         {
19             synchronized (Singleton3.class)
20             {
21                 if(null == instance)
22                 {
23                     //双重校验
24                     instance = new Singleton3();
25                 }
26                 
27             }
28         }
29         return instance;
30     }
31 
32 }

 

 

 

   在加锁的懒汉模式中,看似解决了线程的并发安全问题,有实现了延迟加载,然而它存在着性能问题。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。因此,我们这里就有了双重校验锁。在上面的双重校验锁代码中,由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回单例对象。

     因此,大部分情况下,调用getInstance()都不会执行到同步代码块中的代码,从而提高了性能

     不过,在这里要提到Java中的指令重排优化。指令重排优化:在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。

     由于指令重拍优化的存在,导致初始化Singleton3和将对象地址付给instance字段的顺序是不确定的。比如:在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址复制给instance字段了,然后该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,得到的是状态不确定的对象,程序就会出错。

  以上就是双重校验锁会失效的原因。不过在JDK1.5及其以后的版本中,增加了volatile关键字。volatile的关键字的一个语义就是禁止指令重排优化,这样就保证了instance变量被赋值的时候已经是初始化的,避免了上面提到的状态不确定的问题。

3.登记式单例模式

代码清单【3】

 1 package com.lxf.singleton;
 2 
 3 import java.util.HashMap;
 4 import java.util.Map;
 5 
 6 /**
 7  * 登记式单例模式   线程安全
 8  * @author Administrator
 9  *就是将该类名进行登记,每次调用前查询,如果存在,则直接使用;不存在,则进行登记。
10  *这里使用Map<String,Class>
11  */
12 public class Singleton4 
13 {
14     private static Map<String, Singleton4> map = new HashMap<String, Singleton4>();
15     
16     /*
17      * 静态语句块,保证其中的内容在类加载的时候运行一次,并且只运行一次。
18      */
19     static 
20     {
21         Singleton4 singleton4 = new Singleton4();
22         map.put(singleton4.getClass().getName(), singleton4);
23     }
24     
25     //保护的默认构造子
26     protected Singleton4 (){}
27     //静态工厂方法,返回此类唯一的实例
28     public static Singleton4 getInstance(String name)
29     {
30         if(null == name)
31         {
32             name = Singleton4.class.getName();
33             System.out.println("name == null  --- > name == " + name);
34         }
35         if(null == map.get(name))
36         {
37             try {
38                 map.put(name, (Singleton4) Class.forName(name).newInstance());
39             } catch (InstantiationException e) {
40                 e.printStackTrace();
41             } catch (IllegalAccessException e) {
42                 e.printStackTrace();
43             } catch (ClassNotFoundException e) {
44                 e.printStackTrace();
45             }
46         }
47         return map.get(name);
48     }
49     
50 }

 

 4.静态内部类单例模式;

代码清单【4】

 1 package com.lxf.singleton;
 2 /**
 3  * 静态内部类单例模式  线程安全
 4  * @author Administrator
 5  */
 6 public class Singleton5 
 7 {
 8     /*
 9      * 内部类,用于实现延迟机制
10      * @author Administrator
11      */
12     private static class SingletonHolder
13     {
14         private static Singleton5 instance = new Singleton5();
15     }
16     //私有的构造方法,保证外部的类不能通过构造器来实例化
17     private Singleton5(){}
18     
19     /*
20      *获取单例对象的实例
21      */
22     public static Singleton5 getInstacne()
23     {
24         return SingletonHolder.instance;
25     }
26 
27 }

  这种方式同样利用了类加载机制来保证只创建一个insatcne实例。因此不存在多线程并发的问题。它是在内部类里面去创建对象实例,这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类
也就不会加载单例对象,从而实现了延迟加载。

 

5.枚举类型的单例模式。

代码清单【5】

package com.lxf.singleton;

/**
 * 我们要创建的单例类资源,比如:数据库连接,网络连接,线程池之类的。   
 * @author Administrator
 *
 */
class Resource
{
    public void doMethod()
    {
        System.out.println("枚举类型的单例类资源");
    }
}

/**
 * 枚举类型的单例模式   线程安全
 * 
 * 获取资源的方式,Singleton6.INSTANCE.getInstance();即可获得所要的实例。
 * @author Administrator
 *
 */


public enum Singleton6
{
    INSTANCE;
    
    private Resource instance;
    
    Singleton6()
    {
        instance = new Resource();
    }
    
    public Resource getInstance()
    {
        return instance;
    }
    
    
}

  上面代码中,首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也就会被实例化一次。
  在之前介绍的实现单例的方式中都有共同的缺点:
(1).需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例;
(2).可以使用反射强行调用私有构造器(如果要避免这个情况,可以修改构造器,让它在创建第二个人实例的时候抛异常。)这个会在第5点中进行介绍
  而使用枚举出了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。

6.单例模式测试类   SingletonMain.java

 代码清单【6】

  1 package com.lxf.singleton;
  2 
  3 import org.junit.Test;
  4 
  5 public class SingletonMain 
  6 {
  7     /**
  8      *1. 饿汉模式单例测试
  9      */
 10     @Test
 11     public void testSingletonTest()
 12     {
 13         System.out.println("-------饿汉模式单例测试--------------");
 14         Singleton singleton = Singleton.newInstance();
 15         //singleton.about();
 16         Singleton singleton2 = Singleton.newInstance();
 17         //singleton2.about();
 18         
 19         if(singleton == singleton2)
 20         {
 21             System.out.println("1.singleton and singleton2 are same Object");
 22         }
 23         else
 24         {
 25             System.out.println("1.singleton and singleton2 aren't same Object");
 26         }
 27         System.out.println("---------------------------------------------");
 28     }
 29     
 30     /**
 31      *2. 懒汉式单例模式测试
 32      */
 33     @Test
 34     public void testSingleton2Test()
 35     {
 36         System.out.println("-------懒汉式单例模式测试--------------");
 37         Singleton2 singleton = Singleton2.getInstance();
 38         Singleton2 singleton2 = Singleton2.getInstance();
 39         
 40         if(singleton == singleton2)
 41         {
 42             System.out.println("2.singleton and singleton2 are same Object");
 43         }
 44         else
 45         {
 46             System.out.println("2.singleton and singleton2 aren't same Object");
 47         }
 48         System.out.println("---------------------------------------------");
 49     }
 50     
 51     /**
 52      * 3.双重校验锁单例模式测试
 53      */
 54     @Test
 55     public void testSingleton3()
 56     {
 57         System.out.println("-------双重校验锁单例模式测试--------------");
 58         Singleton3 singleton = Singleton3.getInstance();
 59         Singleton3 singleton2 = Singleton3.getInstance();
 60         
 61         if(singleton == singleton2)
 62         {
 63             System.out.println("3.singleton and singleton2 are same Object");
 64         }
 65         else
 66         {
 67             System.out.println("3.singleton and singleton2 aren't same Object");
 68         }
 69         System.out.println("---------------------------------------------");
 70     }
 71     
 72     /**
 73      * 4.登记式单例模式测试
 74      */
 75     @Test
 76     public void testSingleton4()
 77     {
 78         System.out.println("-------双重校验锁单例模式测试--------------");
 79         Singleton4 singleton = Singleton4.getInstance(Singleton4.class.getName());
 80         Singleton4 singleton2 = Singleton4.getInstance(Singleton4.class.getName());
 81         if(singleton == singleton2)
 82         {
 83             System.out.println("4.singleton and singleton2 are same Object");
 84         }
 85         else
 86         {
 87             System.out.println("4.singleton and singleton2 aren't same Object");
 88         }
 89         System.out.println("---------------------------------------------");
 90     }
 91     
 92     /**
 93      *5. 静态内部类单例模式测试
 94      */
 95     @Test
 96     public void testSingleton5()
 97     {
 98         System.out.println("-------静态内部类单例模式测试--------------");
 99         Singleton5 singleton = Singleton5.getInstacne();
100         Singleton5 singleton2 = Singleton5.getInstacne();
101         if(singleton == singleton2)
102         {
103             System.out.println("5.singleton and singleton2 are same Object");
104         }
105         else
106         {
107             System.out.println("5.singleton and singleton2 aren't same Object");
108         }
109         System.out.println("---------------------------------------------");
110     }
111     
112     /**
113      *6. 静态内部类单例模式测试
114      */
115     @Test
116     public void testSingleton6()
117     {
118         System.out.println("-------枚举类型的单例类资源测试--------------");
119         Resource singleton = Singleton6.INSTANCE.getInstance();
120         Resource singleton2 = Singleton6.INSTANCE.getInstance();
121         if(singleton == singleton2)
122         {
123             System.out.println("6.singleton and singleton2 are same Object");
124         }
125         else
126         {
127             System.out.println("6.singleton and singleton2 aren't same Object");
128         }
129         System.out.println("---------------------------------------------");
130     }
131     
132 
133 }

 

运行结果:

 

5.拓展--如何防止Java反射机制对单例类的攻击?

  上面介绍的除了最后一种枚举类型单例模式外,其余的写法都是基于一个条件:确保不会被反射机制调用私有的构造器。
那么如何防止Java反射机制对单例类的攻击呢?请参考下一篇随笔:《如何防止反射机制对单例类的攻击?》

 

 6.后期补充

 

posted on 2016-12-31 19:52  小李探花IU  阅读(743)  评论(0编辑  收藏  举报