zoukankan      html  css  js  c++  java
  • 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.后期补充

  • 相关阅读:
    基础知识
    显示功能
    监听器
    检测session是否为空
    W7-Web服务器[JavaWeb]
    D9-哈希表[Java数据结构和算法]
    D8-查找算法[Java数据结构和算法]
    D7-排序算法(三)[Java数据结构和算法]
    W6-junit、泛型、枚举、增强for、可变参数、反射[JavaWeb]
    D6-排序算法(二)[Java数据结构和算法]
  • 原文地址:https://www.cnblogs.com/lthIU/p/6240066.html
Copyright © 2011-2022 走看看