单例模式杂谈

前言

今天来聊聊一种经常用的设计模式——单例模式,以及它的两种实现方式,饿汉模式和懒汉模式。

那么什么是单例模式?单例模式又有哪些优点?接下来我们一点点来讲解。

目录

前言

单例模式

一、单例模式简介

二、单例模式的简单实现

单例模式的实现——懒汉模式

单例模式的实现——饿汉模式

单例模式的实现——饿汉模式+静态内部类

总结


单例模式

一、单例模式简介

假如我们在一个项目中,需要读取一下配置文件,暂定这个类叫ConfigManager.java,那么如果不是单例模式的,则需要每次都去new一个此类的实例。这些都是固定的配置参数,对于每一个线程都是一样的,大家完全可以共享一个实例,并且读取配置文件属于I/O操作,要知道I/O操作本来就是很昂贵的,如果每次调用都去读取一次,会严重影响系统的性能。

那么可不可以将这个类只实例化一次,而且读取配置文件的操作只执行一次,然后就拿到内存中供所有人使用呢?

单例模式就是用来解决这种问题的。它是23中设计模式之一,也是最常用的一种设计模式。顾名思义,单例模式就是在系统运行期间,有且仅有一个实例。它有三个必须满足的条件:

  1. 一个类只有一个实例;
  2. 自身创建自身的实例;
  3. 自行向整个系统提供自身实例。

这三个条件,到底怎么满足呢?接下来我们以一个例子,来学习单例模式。

二、单例模式的简单实现

假设现在我们要读取数据库配置文件——data.properties。

首先在src目录下新建data.properties文件:

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/ShopOrder?useSSL=true&serverTimezone=UTC
username=root
password=****

然后创建ConfigManager.java文件:

package Demo;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 *  单例模式
 *  负责读取数据库连接信息
 * */
public class ConfigManager {
    private static ConfigManager configManager;  
    private static Properties properties;

    /**
     *  私有构造函数,执行在整个程序运行期间只需要进行一次的操作
     * */
    private ConfigManager(){
        properties=new Properties();
        InputStream inputStream=ConfigManager.class.getClassLoader().getResourceAsStream("data.properties");
        try {
            properties.load(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     *  对外公开的方法
     * */
    public static ConfigManager getInstance(){
        if (configManager==null)
            configManager=new ConfigManager();  //自身创建自身的实例
        return configManager;
    }


    /**
     *  返回读取到的值
     * */
    public static String getValue(String key){
        return properties.getProperty(key);
    }
}

最后创建测试类:

package Demo;

import org.junit.Test;

public class ConfigTest {

    @Test
    public void doTest(){
        System.out.println(ConfigManager.getInstance().getValue("username"));
    }
}

运行结果会输出字符串“root”。接下来说说我们是怎样满足那三个条件的。

1、一个类只有一个实例。这是满足单例模式最基本的要求,若要满足这一点,我们只能提供私有的构造函数,保证不能随意创建该类的实例。并且我们在构造函数中执行了读取配置文件的操作,这样就保证了I/O操作只执行一次。

2、自身创建自身的实例。对于这一点,正是体现了“有且仅有一个实例”的特性。我们要保证唯一性,也就意味着必须提供一个实例并由它自身创建,所以我们定义了一个ConfigManager类型的静态变量,以便向外界提供实例时使用。

3、自行向整个系统提供自身实例。这一点是至关重要的,因为我们设置了ConfigManager类的构造器是私有的,所以外界是无法通过new操作符去获得它的实例的。那么就必须提供一个静态公有方法,该方法创建自身的实例并返回。所以我们定义了一个全局访问点getInstance()方法,该方法返回该类的实例,并做了逻辑判断,如果不为null则直接返回,不需要再创建实例。

到这一步,我们已经可以获取ConfigManager类的实例了,并调用它的getValue方法,获取从配置文件读到的值。

以上就是最简单的单例模式实现,但是在并发环境下,它有很严重的弊端。比如线程不安全,可能会出现多个ConfigManager的实例,所以在实际开发中不会采用这种单例模式的实现。那么如何解决这个问题呢?

这就需要再学习单例模式的两种实现——懒汉模式和饿汉模式。

单例模式的实现——懒汉模式

所谓懒汉模式,正如其名,比较懒,在类加载的时候并不创建自身实例,采用延迟加载的方式,只有在运行时调用了相关的方法才会被动创建自身的实例。上述的示例我们就采用了懒汉模式,只有我们调用全局访问点getInstance()方法的时候,这个类的实例才被创建。当然,在这种情况下虽然保证了其延迟加载的特性,但是存在线程安全问题,即我们上文提到的,在多线程下无法正常工作,这也是致命的缺陷。

现在对上面的示例进行修改,只修改getInstance()方法即可,因为最简单的方法就是考虑同步,这里采用synchronized实现。

/**
*  对外公开的方法,延迟加载
* */
public static synchronized ConfigManager getInstance(){
    if (configManager==null)
        configManager=new ConfigManager();
    return configManager;
}

以上写法能够在多线程并发环境中很好地工作,并且看起来它也具备了延迟加载的特性。但是很遗憾,这种处理方式效率不高,可以说95%以上的情况都不需要同步。

那么对于线程安全问题,还有另一种解决方式,即饿汉模式。

单例模式的实现——饿汉模式

饿汉模式在类加载的时候就已经完成了初始化操作,所以类加载较慢,但是获取对象的速度很快。由于饿汉模式在类初始化时就完成了实例化,所以它是不存在线程安全问题的。修改以上的代码:

package Demo;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 *  单例模式
 *  负责读取数据库连接信息
 * */
public class ConfigManager {
    private static ConfigManager configManager=new ConfigManager();  //加载时即实例化
    private static Properties properties;

    /**
     *  私有构造函数,执行在整个程序运行期间只需要进行一次的操作
     * */
    private ConfigManager(){
        properties=new Properties();
        InputStream inputStream=ConfigManager.class.getClassLoader().getResourceAsStream("data.properties");
        try {
            properties.load(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     *  对外公开的方法,延迟加载
     * */
    public static ConfigManager getInstance(){
        return configManager;
    }


    /**
     *  返回读取到的值
     * */
    public static String getValue(String key){
        return properties.getProperty(key);
    }
}

上述代码中修改了getInstance()方法,直接返回configManager,而此实例在类加载时就已经自行实例化了。这种方式基于classloader机制,有效避免了多线程的同步问题。

但是,由于导致类加载的原因比较多,而此时单例类ConfigManager在类加载时就实例化,显然没有达到延迟加载的效果。

现在可以对比一下两种方式:

  • 懒汉模式,在类加载时不创建实例,因此类加载速度快,但是运行时获取对象的速度较慢,具备延迟加载的特性,但是又存在线程不安全的问题。
  • 饿汉模式在类加载时就完成初始化,所以类加载较慢,但是获取对象的速度很快。
  • 懒汉模式是“时间换空间”,饿汉模式是“空间换时间”,因为一开始就创建了实例,所以每次使用时直接返回该实例就好了。

在实际开发场景中,实例化单例类很消耗资源,我们希望它可以延迟加载,显然饿汉模式并不能实现。那么我们应该怎么处理呢?

要想让饿汉模式同时具备延迟加载的特性,可以搭配静态内部类进行改造实现。

单例模式的实现——饿汉模式+静态内部类

直接上代码:

package Demo;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 *  单例模式
 *  负责读取数据库连接信息
 * */
public class ConfigManager {
    private static ConfigManager configManager;
    private static Properties properties;

    /**
     *  私有构造函数,执行在整个程序运行期间只需要进行一次的操作
     * */
    private ConfigManager(){
        properties=new Properties();
        InputStream inputStream=ConfigManager.class.getClassLoader().getResourceAsStream("data.properties");
        try {
            properties.load(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     *  静态内部类
     * */
    public static class ConfigManagerHelp{
        private static final ConfigManager INSTANCE=new ConfigManager();  //静态常量
    }

    /**
     *  对外公开的方法,装载静态内部类,延迟加载
     * */
    public static ConfigManager getInstance(){
        configManager=ConfigManagerHelp.INSTANCE;
        return configManager;
    }

    /**
     *  返回读取到的值
     * */
    public static String getValue(String key){
        return properties.getProperty(key);
    }
}

以上代码同样利用了classloader机制来保证初始化INSTANCE时只有一个线程,但是与之前的方式有点不同。

按照之前的方式,只要ConfigManger类被装载了,那么configManger属性就会被实例化,并没有达到延迟加载的效果。而现在采用静态内部类的方式进行改造,在ConfigManger类被装载时不一定进行实例化,因为ConfigManagerHelp类没有被主动调用。只有通过调用了getInstance()方法,才会装载ConfigManagerHelp类,从而进行实例化。而且INSTANCE是静态常量,一旦创建了实例,内存地址是不会变的,在getInstance()方法只需要进行引用即可。

这种方式比懒汉模式、饿汉模式都要好,既实现了线程安全,又实现了延迟加载。

总结

对于单例模式的使用,不管是懒汉模式,饿汉模式,或者静态内部类的方式,根据具体的业务需求而定。反正要遵守一个原则:

在整个程序运行期间,有且仅有一个实例

若违背这一点,那么即使设计的天花乱坠,也不是单例模式的实现。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值