GOF23设计模式之单例模式详解

设计模式在软件设计开发中经常被使用到,是一个高级软件工程师开发项目必备的技能之一,设计模式总共有23种,本篇文章,笔者将介绍第一种设计模式:单例模式的使用。

本篇文章主要围绕下面三个方面来讲解单例模式:

  1. 单例模式的五种方式的使用
  2. 如何使用反射以及反序列化破解单例模式并且防止破解
  3. 五种单例模式创建方式性能的比较

在谈上面几个问题之前,先来看看什么是单例模式以及它的应用场景。

单例模式是创建型模式中的一种模式,该模式保证一个类只有一个实例,并且只提供一个访问该实例的全局访问点。

单例模式的常见应用场景:

  • windows的Tasker Manager(任务管理器)是典型的单例模式。
  • 在项目中读取配置文件的类一般只需要new一次,可以设计为单例模式。
  • 网站的计数器一般为单例模式,否则不能同步。
  • 数据库连接池的设计是一种单例模式,因为数据库连接是一种资源,没必要每次都去创建。
  • 操作系统的文件系统也是一种单例模式的实现,一个系统只有一个文件系统。
  • 在servlet应用中,每一个servlet是单例的。
  • 在spring 框架中,bean的创建也是单例模式的,方便spring容器管理。
  • Spring Mvc框架中控制器也是单例的。

一、 单例模式的五种方式

单例模式总共有五种方式来实现:

  1. 懒汉式
  2. 饿汉式
  3. 双重检测锁实现(基本不用)
  4. 静态内部类实现
  5. 枚举类实现

单例模式实现的几个要点:首先构造器私有,然后就是实例静态化私有,提供一个全局访问实例的静态化方法入口

1.懒汉式
懒汉式:见名知意,就是说在创建实例时不立即创建,当真正需要使用的时候去创建,因此懒汉式是延迟加载,但是在多线程环境下需要同步,因此调用效率不高,下面来看看代码的实现:

package cn.just.Test;
/**
 * 通过实现懒汉式实现单例模式
 * @author Shinelon
 *
 */
public class Demo04 {
    //类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)。
    private static Demo04 instance;
    //私有构造器
    private Demo04(){

    }
    //方法同步,调用效率低
    public static synchronized Demo04 getInstance(){
        return instance;
    }
}

2.饿汉式
饿汉式:与上面懒汉式相对,在类加载时就创建实例,立即加载,没有延迟加载,线程安全,因为它的实例时在类加载时立即创建,而类加载器加载类时本身就是线程安全的(不懂得读者可以查阅类加载器的资料),因此静态访问方法不需要同步,因此调用效率高。下面是实现代码:

package cn.just.Test;
/**
 * 通过饿汉式实现单例模式
 * @author Shinelon
 */
public class Demo5{
     private static Demo5 instance=new Demo5();
     //私有构造器
     private Demo5() {
     }
     //方法没有同步,调用效率高
     public static Demo5 getInstance(){
         return instance;
     }
}

3.双重检测锁实现
双重检测锁是在懒汉式的基础上细化了锁的粒度,提高了并发性和调用效率,线程安全,延迟加载,不过了解并发的读者可以看到下面的代码,在高并发场合会会因为编译器优化和JVM底层内存模型的原因出现问题。因此不建议使用。

package cn.just.Test;
/**
 * 通过双重检测锁形式实现单例模式
 * @author Shinelon
 *
 */
public class Demo6 {
    private static Demo6 instance;
    //私有构造器
    private Demo6(){
        if(instance==null){
            Demo6 sington;
            synchronized (Demo6.class) {
                sington=instance;
                if(instance==null){
                    synchronized (Demo6.class) {
                        if(instance==null){
                            sington=new Demo6();
                        }
                    }
                    instance=sington;
                }
            }
        }

    }
    //方法同步,调用效率低
    public static synchronized Demo6 getInstance(){
        return instance;
    }
}

4.静态内部类实现
静态内部类也是在懒加载的基础上进一步优化,利用加载类的天然线程安全的机制创建内部类来代替方法同步加锁的方式提高调用效率,并且保证线程安全,延迟加载,代码如下:

package cn.just.Test;
/**
 * 测试静态内部类实现单例模式
 * 线程安全,调用效率高,并且实现了延时加载
 * @author Shinelon
 *
 */
public class Demo01 {
     private static class SimplyMode{
         private static final Demo01 demo=new Demo01();
     }
     //方法没有同步,调用效率高
     public  static Demo01 getInstance(){
         return SimplyMode.demo;
     }
    //构造器私有
     private Demo01(){

     }
}

5.枚举实现
最后一种实现方式是利用JVM底层实现的eumn线程安全的特性保证线程安全来优化代码,不过它没有延迟加载,但是线程安全,调用效率高。它也可以防止利用反射和反序列化来破解单例模式(下文会具体讲到),代码如下:

package cn.just.Test;
/**
 * 测试枚举式实现单例模式
 * 避免了反射和反序列化的漏洞(可以通过反射调用私有的构造器)
 * 没有延时加载
 * @author Shinelon
 *
 */
public enum Demo02 {
    //这个枚举元素就是一个单例对象
      INSTANCE;
    //添加自己需要的操作
    public void Operation(){

    }
}

在介绍完上面五种单例模式的实现方式之后,下面我们将会讲到如何使用反射和反序列化来破解单例模式以及如何防止。

二、如何使用反射以及反序列化破解单例模式并且防止破解

1.使用反射
熟悉反射的读者都知道,尽管你构造器私有,但是我们只要赋予访问权限,任然可以在外部访问其构造器,因此,就是利用这一特性来破解单例模式。代码如下:

/**
 * 利用反射破解单例模式
 * @author Shinelon
 *
 */
public class Client02 {
    public static void main(String[] args) throws  Exception{
        Demo5 d1=Demo5.getInstance();
        Demo5 d2=Demo5.getInstance();
        System.out.println("测试饿汉式");
        System.out.println(d1);
        System.out.println(d2);
//      通过反射来破解单例模式
        Class<Demo5> clazz=(Class<Demo5>) Class.forName("cn.just.Test.Demo5");
        Constructor<Demo5> c=clazz.getDeclaredConstructor(null);
        c.setAccessible(true);          //可以设置访问权限从而来获得私有的构造器
        Demo5 d3=c.newInstance();
        Demo5 d4=c.newInstance();
        System.out.println(d3);
        System.out.println(d4);
        }
    }

上面的代码执行如下结果如下:
这里写图片描述
很明显,我们可以看到,使用反射创建的两个对象是两个不同的对象,由于可见如何使用反射破解单例模式。那么如何防止使用反射来破解单例模式呢?很简单,只需要在构造器中加入判定条件即可防止:

public class Demo5{
     private static Demo5 instance=new Demo5();
     //私有构造器
     private Demo5() {
       if(instance!=null){                    //也可通过设置判断来不让用反射机制获取构造器
             throw new RuntimeException();
         }
     }
     //方法没有同步,调用效率高
     public static Demo5 getInstance(){
         return instance;
     }
}

2.使用反序列化
我们也可以使用反序列化来破解单例模式,直接来看代码如何实现:

package cn.just.Test;
/**
 * 利用反序列化破解单例模式
 * @author Shinelon
 *
 */
public class Client02 {
    public static void main(String[] args) throws  Exception{
        Demo5 d1=Demo5.getInstance();
        Demo5 d2=Demo5.getInstance();
        System.out.println("测试饿汉式");
        System.out.println(d1);
        System.out.println(d2);
        //通过反序列化来破解单例模式
        FileOutputStream fos=new FileOutputStream("E:/a.txt");
        ObjectOutputStream os=new ObjectOutputStream(fos);
        os.writeObject(d1);
        os.close();
        fos.close();
        ObjectInputStream is=new ObjectInputStream(new FileInputStream("E:/a.txt"));
        Demo5 d3=(Demo5) is.readObject();
        is.close();
        System.out.println(d3);

    }
}

上面代码执行结果如下:
这里写图片描述
从执行结果可以看出来通过反序列化同一个对象之后得到与之前不同的对象实例,由此可见如何使用反序列化破解单例模式,那么如何防止呢?可以创建一个叫readResolve()的函数,该函数在反序列化时不会创建一个新的对象,而会返回之前创建的对象的实例。代码如下:

public class Demo5 implements Serializable{
     private static Demo5 instance=new Demo5();
     //私有构造器
     private Demo5() {
     }
     //方法没有同步,调用效率高
     public static Demo5 getInstance(){
         return instance;
     }
     //反序列化时实现了该方法(回调函数)就会返回之前创建的对象
     private Object readResolve() {
         return instance;
    }
}

最后来看看这几种实现单例模式方式的性能,如何选用哪种方式来实现单例模式。

三、五种实现方式的性能的比较

下面测试在多线程环境下这几种方式的效率,使用了JDK并发包中的CountDownLatch工具类(不了解该类的读者可以参考相关资料)来测试,创建10个线程,每个线程调用100000次单例模式实现类来测试效率。先看看测试代码:

package cn.just.Test;

import java.util.concurrent.CountDownLatch;
/**
 * 测试多线程环境下五种创建单例模式的效率
 * @author Shinelon
 *
 */
public class Client03 {
    public static void main(String[] args) throws Exception{
        long start=System.currentTimeMillis();
        int threadcount=10;          //启动十个线程
        final CountDownLatch  countDownLatch = new CountDownLatch(threadcount);
        for(int Tcount=0;Tcount<threadcount;Tcount++){
       new Thread(new Runnable(){
        @Override
        public void run() {
            for(int i=0;i<1000000;i++){
                Object o=Demo04.getInstance();
            }
            countDownLatch.countDown();        //每执行完一条线程,线程数减一
        }

       }).start();
    }
        countDownLatch.await(); //main线程阻塞,直到计数器变为0,才会继续往下执行!
        long end =System.currentTimeMillis();
        System.out.println("总共耗时:"+(end-start));
    }
}

下面是在64位Windows操作系统上测试多次取平均值的结果(不同的环境可能结果不同,但是他们之间的差距基本一样):

懒汉式 160ms
饿汉式 30ms
静态内部类 25ms
枚举 15ms

因此,我们可以得出下面的结论;

当单例对象占用资源少,经常要使用,即不需要延迟加载时可以使用枚举来代替饿汉式模式。
当单例对象占用资源多,不是经常使用,可以使用延迟加载方式,使用静态内部类方式来替代懒汉式模式提高效率。

以上就是个人对单例模式的理解与总结,请尊重劳动成果,转载标明原文链接与地址。谢谢阅读!!!


猜你喜欢

转载自blog.csdn.net/qq_37142346/article/details/80161291