Java23种设计模式(GOF)——单例模式

核心作用

保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。

常见应用场景

1、windows系统的任务管理器和回收站就是很典型的单例模式;
2、项目中读取配置文件的类,一般也只有一个对象,没必要每次使用配置文件数据,每次new一个对象去读取;
3、网站的计数器,一般也是采用单例模式实现,否则难以同步;
4、应用程序的日志应用,一般都采用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加;
5、数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源;
6、Servlet编程中的Application、每个Servlet也是单例模式的典型应用
7、在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理;
8、在SpringMVC中,控制器对象也是单例。

优点

由于单例模式只生成一个实例,减少了系统的开销,当一个对象的产生需要比较多的资源时,比如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决

单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理

常见的五种单例模式实现方式

主要

  • 饿汉式(线程安全,调用效率高。但是,不能延时加载。)
  • 懒汉式(线程安全,调用效率不高,资源利用率高了。但是,可以延时加载。)

其他

  • 双重检测锁式(由于JVM底层内部模式原因,偶尔会出问题,不建议使用)
  • 静态内部类式(线程安全,调用效率高,可以延时加载)
  • 枚举单例(线程安全,调用效率高,不能延时加载,可以避免反射和反序列化的漏洞)

选用方式

单例对象 占用资源少,不需要延时加载
枚举式 好于 饿汉式

单例对象 占用资源大 需要延时加载
静态内部类式 好于 懒汉式

案例

1、饿汉式

/**
 * 饿汉式单例模式(单例对象立即加载)
 * @author huangyzh
 * @create 2020-05-13 0:07
 */
public class SingletonDemo01 {

    //类初始化时,立即加载这个对象(没有延时加载的优势)
    //由于加载类时,天然的是线程安全的
    private static SingletonDemo01 instance = new SingletonDemo01();

    /**
     * 私有构造器
     */
    private SingletonDemo01(){}

    /**
     * 方法没有同步,调用效率高
     * @return
     */
    public static SingletonDemo01 getInstance(){
        return instance;
    }
}

饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会设计多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此可以省略synchronized关键字

问题:r如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!

2、懒汉式

/**
 * 懒汉式单例模式(单例对象延迟加载)
 * @author huangyzh
 * @create 2020-05-13 0:15
 */
public class SingletonDemo02 {

    //类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
    private static SingletonDemo02 instance;

    /**
     * 私有构造器
     */
    private SingletonDemo02(){}

    //方法同步,调用效率不高,资源利用率高
    public static synchronized SingletonDemo02 getInstance(){
        if(instance == null){
            instance = new SingletonDemo02();
        }
        return instance;
    }
}

要点:lazy load! 延迟加载! 懒加载! 真正用的时候才加载!
问题:资源利用率高了。但是,每次调用getInstance()方法都要同步,并发效率较低。

3、双重检测锁式

/**
 * 双重检测锁式(不建议使用,了解即可)
 * @author huangyzh
 * @create 2020-05-13 0:19
 */
public class SingletonDemo03 {

    private static SingletonDemo03 instance;

    /**
     * 私有构造器
     */
    private SingletonDemo03(){}

    public static SingletonDemo03 getInstance(){
        if(instance == null){
            SingletonDemo03 sd;
            synchronized (SingletonDemo03.class){
                sd = instance;
                if(sd == null){
                    synchronized (SingletonDemo03.class){
                        if(sd == null){
                            sd = new SingletonDemo03();
                        }
                    }
                    instance = sd;
                }
            }
        }
        return instance;
    }
}

这个模式将同步内容下放到if内部,提供了执行的效率,不必每次获取对象时都进行同步,只有第一次才同步创建了以后就没必要了。

问题:由于编译器优化原因和JVM底层内部模型原因,偶尔会出问题,不建议使用。

4、静态内部类式

/**
 * 静态内部类式(线程安全,调用效率高,并且实现了延时加载)
 * @author huangyzh
 * @create 2020-05-13 0:25
 */
public class SingletonDemo04 {

    private static class SingletonClassInstance{
        private static final SingletonDemo04 instance = new SingletonDemo04();
    }
    

    /**
     * 私有构造器
     */
    private SingletonDemo04(){}
    
    public static SingletonDemo04 getInstance(){
        return SingletonClassInstance.instance;
    }
}

要点:
1、外部类没有static属性,则不会像饿汉式那样立即加载对象。
2、只要真正调用getInstance()才会加载静态内部类。加载类时是线程安全的。instace是static final类型,保证了内存中只要这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。
3、兼备了并发高效调用和延迟加载的优势!

破解单例问题

1)反射方式

反射可以破解上面几种实现方式:
可以在构造器中收到抛出异常控制

测试代码
/**
 * 测试反射破解单例
 * @author huangyzh
 * @create 2020-05-13 1:25
 */
public class SigletonTest01 {
    public static void main(String[] args) throws Exception {
        SingletonDemo02 s1 = SingletonDemo02.getInstance();
        SingletonDemo02 s2 = SingletonDemo02.getInstance();
        System.out.println(s1);//edu.yunyu.singleton.SingletonDemo@1b6d3586
        System.out.println(s2);//edu.yunyu.singleton.SingletonDemo@1b6d3586
        System.out.println(s1 == s2);//true
        //通过反射的方式直接调用私有构造器
        Class<SingletonDemo02> clazz = (Class<SingletonDemo02>) Class.forName("edu.yunyu.singleton.SingletonDemo02");
        Constructor<SingletonDemo02> constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);//跳过权限检查

        SingletonDemo02 s3 = constructor.newInstance();
        SingletonDemo02 s4 = constructor.newInstance();
        System.out.println(s3);//edu.yunyu.singleton.SingletonDemo@4554617c
        System.out.println(s4);//edu.yunyu.singleton.SingletonDemo@74a14482
        System.out.println(s3 == s4);//false
    }
}
阻止反射破解
/**
 * 懒汉式单例模式(单例对象延迟加载)
 * @author huangyzh
 * @create 2020-05-13 0:15
 */
public class SingletonDemo06 {

    //类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
    private static SingletonDemo06 instance;

    /**
     * 私有构造器,阻止反射破解单例
     */
    private SingletonDemo06(){
        if(instance != null){
            throw new RuntimeException();
        }
    }

    //方法同步,调用效率不高,资源利用率高
    public static synchronized SingletonDemo06 getInstance(){
        if(instance == null){
            instance = new SingletonDemo06();
        }
        return instance;
    }
}
2)反序列化方式

反序列化可以破解上面几种实现方式:
反序列化时,如果对象所在类定义了readResolve(),(实际是一种回调),定义返回哪个对象

测试代码
/**
 * 测试反序列化破解单例
 * @author huangyzh
 * @create 2020-05-13 1:25
 */
public class SigletonTest02 {
    public static void main(String[] args) throws Exception {
        SingletonDemo02 s1 = SingletonDemo02.getInstance();
        SingletonDemo02 s2 = SingletonDemo02.getInstance();
        System.out.println(s1);//edu.yunyu.singleton.SingletonDemo@1b6d3586
        System.out.println(s2);//edu.yunyu.singleton.SingletonDemo@1b6d3586
        System.out.println(s1 == s2);//true

        //通过反序列化的方式构造多个对象
        FileOutputStream fos = new FileOutputStream("D:/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s1);
        oos.close();
        fos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/a.txt"));
        SingletonDemo02 s3 = (SingletonDemo02)ois.readObject();
        System.out.println(s3);//edu.yunyu.singleton.SingletonDemo@6d03e736
    }
}
阻止反序列化破解
/**
 * 懒汉式单例模式(单例对象延迟加载)
 * 阻止反序列化
 * @author huangyzh
 * @create 2020-05-13 0:15
 */
public class SingletonDemo07 implements Serializable {

    //类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
    private static SingletonDemo07 instance;

    /**
     * 私有构造器,阻止反射破解单例
     */
    private SingletonDemo07(){
        if(instance != null){
            throw new RuntimeException();
        }
    }

    //方法同步,调用效率不高,资源利用率高
    public static synchronized SingletonDemo07 getInstance(){
        if(instance == null){
            instance = new SingletonDemo07();
        }
        return instance;
    }

    //反序列化时,如何定义了readResolve()则直接返回此方法指定对象,而不需要单独再创建新对象
    private Object readResolve(){
        return instance;
    }

}

5、枚举方式

package edu.yunyu.singleton;

/**
 * 枚举单例(避免反射和反序列化的漏洞,无延时加载)
 * @author huangyzh
 * @create 2020-05-13 0:35
 */
public enum SingletonDemo05 {

    //这个枚举元素,本身就是单例对象
    INSTANCE;

    public void singletonOperation(){
        //添加操作
    }

}

优点:
实现简单
枚举本身就是单例模式,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞

缺点:
无延迟加载

多线程环境下五种单例模式的效率测试

/**
 * 测试多线程环境下五种创建单例模式的效率
 * @author huangyzh
 * @create 2020-05-13 1:25
 */
public class SigletonTest03 {
    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        int threadNum =10;
        final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        Object o1 = SingletonDemo01.getInstance();
//                        Object o2 = SingletonDemo02.getInstance();
//                        Object o3 = SingletonDemo03.getInstance();
//                        Object o4 = SingletonDemo04.getInstance();
//                        Object o5 = SingletonDemo05.INSTANCE;
                    }
                    countDownLatch.countDown();
                }
            }).start();
        }

        countDownLatch.await();//main线程阻塞,直到计算器变为0,才会继续往下执行

        long end = System.currentTimeMillis();
        System.out.println("总耗时:"+(end-start));
    }
}
模式毫秒数
饿汉式22ms
懒汉式636ms
静态内部类式28ms
枚举式32ms
双重检测锁式65ms

关注相对值即可,在不同的环境下不同的程序测试的结果可能完全不一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值