前言
今天来聊聊一种经常用的设计模式——单例模式,以及它的两种实现方式,饿汉模式和懒汉模式。
那么什么是单例模式?单例模式又有哪些优点?接下来我们一点点来讲解。
目录
单例模式
一、单例模式简介
假如我们在一个项目中,需要读取一下配置文件,暂定这个类叫ConfigManager.java,那么如果不是单例模式的,则需要每次都去new一个此类的实例。这些都是固定的配置参数,对于每一个线程都是一样的,大家完全可以共享一个实例,并且读取配置文件属于I/O操作,要知道I/O操作本来就是很昂贵的,如果每次调用都去读取一次,会严重影响系统的性能。
那么可不可以将这个类只实例化一次,而且读取配置文件的操作只执行一次,然后就拿到内存中供所有人使用呢?
单例模式就是用来解决这种问题的。它是23中设计模式之一,也是最常用的一种设计模式。顾名思义,单例模式就是在系统运行期间,有且仅有一个实例。它有三个必须满足的条件:
- 一个类只有一个实例;
- 自身创建自身的实例;
- 自行向整个系统提供自身实例。
这三个条件,到底怎么满足呢?接下来我们以一个例子,来学习单例模式。
二、单例模式的简单实现
假设现在我们要读取数据库配置文件——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()方法只需要进行引用即可。
这种方式比懒汉模式、饿汉模式都要好,既实现了线程安全,又实现了延迟加载。
总结
对于单例模式的使用,不管是懒汉模式,饿汉模式,或者静态内部类的方式,根据具体的业务需求而定。反正要遵守一个原则:
在整个程序运行期间,有且仅有一个实例。
若违背这一点,那么即使设计的天花乱坠,也不是单例模式的实现。