一个有价值的系统总是会因为需求的变化而变化,可能是原有需求的修改,也可能是新需求的增加。于是可怜的猿们就得修改原来的代码。好的架构和设计可以让我们的代码结构具有良好的扩展性,在满足需求变化的同时仅需要修改尽可能少的代码,可以将需求变化对原系统的影响降到很低。设计模式就是人们对于良性架构设计的经验总结。
单例模式的特点主要是:一是某个类只能有一个实例;二是这个类必须自己创建这个实例;三是它必须自行向整个系统提供这个实例。
有时候需要一个类为系统提供服务,它通常不是为某个特定模块服务,而是整个系统多个地方可能都会需要这个服务。
最常见的就是系统配置,我们会把配置放在一个配置文件,有多个模块都会读取。为了资源的不浪费,这个类不应该有多个实例,因为这个类的表现只和那个配置文件有关,而配置文件只有一个,多个实例会浪费资源。那么这个唯一的实例到底谁去创建?思考一下会发现,对于使用这个配置功能的角色来说,他们需要得到实例,但是不能直接去实例化,因为那样就不能保证只有一个实例了。还必须看是否已经有一个实例了,如果有就直接用,没有就需要创建一个。但是这些事情每个客户端角色都去自己判断?最合理的方式就是这个配置类自己管理自己的实例,并提供取得这个实例的方法。这些需求刚好就是单例模式的特点。
配置文件system.cfg
key1=value1
key2=value2
/**
* 饿汉式的单例配置管理器
*/
public class ConfigManager {
// 配置文件
private static final String CONFIG_FILE_NAME = "system.cfg";
// 唯一的实例
private static final ConfigManager INSTANCE = new ConfigManager();
/**
* 将配置读取到内存
*/
private final Properties configs;
private ConfigManager() {
System.out.println("实例化配置管理器");
configs = new Properties();
try {
configs.load(ConfigManager.class.getClassLoader().getResourceAsStream(CONFIG_FILE_NAME));
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* 取得配置文件名
* @return
*/
public static String getConfigFileName() {
return CONFIG_FILE_NAME;
}
/**
* 取得实例
* @return
*/
public static ConfigManager getInstance() {
return INSTANCE;
}
/**
* 取得配置
* @param configKey
* @return
*/
public String getConfigValue(String configKey) {
return configs.getProperty(configKey);
}
}
/**
* 客户端
*/
public class Client {
/**
* 打印配置文件名
*/
public void printConfigFileName() {
System.out.println(ConfigManager.getConfigFileName());
}
/**
* 打印配置值
* @param key
*/
public void printConfigValue(String key) {
System.out.println(ConfigManager.getInstance().getConfigValue(key));
}
public static void main(String[] args) {
Client client = new Client();
client.printConfigFileName();
}
}
饿汉式最简单,但是当类加载时便会实例化,然而我们可能在本次系统生命周期内都不需要它实例化。
/**
* 饿汉式的单例配置管理器
*/
public class ConfigManager {
// 配置文件
private static final String CONFIG_FILE_NAME = "system.cfg";
// 唯一的实例
private static ConfigManager INSTANCE;
/**
* 将配置读取到内存
*/
private final Properties configs;
private ConfigManager() {
System.out.println("实例化配置管理器");
configs = new Properties();
try {
configs.load(ConfigManager.class.getClassLoader().getResourceAsStream(CONFIG_FILE_NAME));
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* 取得配置文件名
* @return
*/
public static String getConfigFileName() {
return CONFIG_FILE_NAME;
}
/**
* 取得实例
* @return
*/
public static synchronized ConfigManager getInstance() {
if (INSTANCE == null) {
INSTANCE = new ConfigManager();
}
return INSTANCE;
}
/**
* 取得配置
* @param configKey
* @return
*/
public String getConfigValue(String configKey) {
return configs.getProperty(configKey);
}
}
/**
* 客户端
*/
public class Client {
/**
* 打印配置文件名
*/
public void printConfigFileName() {
System.out.println(ConfigManager.getConfigFileName());
}
/**
* 打印配置值
* @param key
*/
public void printConfigValue(String key) {
System.out.println(ConfigManager.getInstance().getConfigValue(key));
}
public static void main(String[] args) {
Client client = new Client();
client.printConfigFileName();
client.printConfigValue("key1");
}
}
懒汉式单例模式可以在真正调用实例方法时才实例化,但是为了防止同一时间多个线程同时调用getInstance方法,造成实例化多次,必须将该方法声明为同步方法,这是个大的性能损失。
懒汉式的getInstance方法,其实只有第一次调用需要同步,一旦实例化完成,之后并不需要同步,这样的性能开销在之后完全是浪费。
/**
* DCL的单例配置管理器
*/
public class ConfigManager {
// 配置文件
private static final String CONFIG_FILE_NAME = "system.cfg";
// 唯一的实例
private static ConfigManager INSTANCE;
/**
* 将配置读取到内存
*/
private final Properties configs;
private ConfigManager() {
System.out.println("实例化配置管理器");
configs = new Properties();
try {
configs.load(ConfigManager.class.getClassLoader().getResourceAsStream(CONFIG_FILE_NAME));
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* 取得配置文件名
* @return
*/
public static String getConfigFileName() {
return CONFIG_FILE_NAME;
}
/**
* 取得实例
* @return
*/
public static ConfigManager getInstance() {
if (INSTANCE == null) {// step1
synchronized (ConfigManager.class) {// step2
if (INSTANCE == null) {// step3
INSTANCE = new ConfigManager();// error
}
}
}
return INSTANCE;
}
/**
* 取得配置
* @param configKey
* @return
*/
public String getConfigValue(String configKey) {
return configs.getProperty(configKey);
}
}
在第一调用getInstance方法期间,可能多个线程到达step1,都检查通过,但是在step2时进行锁竞争,只有一个线程可以进入step3,这个得到锁的线程执行了实例化,完成同步块代码的执行后,之前在step2等待的线程继续竞争锁,又有一个线程得到锁,但是这时候进行第二次检查时,发现INSTANCE已经不为空,不再进行实例化。之后再有线程调用getInstance方法,在第一次检查判空就会跳过返回实例。不再有同步代码,从而巧妙的避开同步方法的开销。
看似天衣无缝,但是这里有一个隐患,在注释着error的那一行代码,并不是一个原子操作,其实包含多个子步骤:
1.分配内存。
2.初始化对象实例。
3.将对象在内存的地址赋值给引用,也就是将INSTANCE指向对象。
有时候编译器或者虚拟机可能出于性能等原因,会重排这三步顺序,可能将第二步排到第三步之后。
那么在第一次调用getInstance方法期间,如果得到锁的线程恰好执行到error那行,INSTANCE已经不为空,但是对象未初始化完成,这时候其他线程调用getInstance方法进行第一次判空就会通过,这时候调用实例方法就可能发生不可预料的异常。
为了保证有序性,我们可以使用关键字volatile
/**
* DCL的单例配置管理器
*/
public class ConfigManager {
// 配置文件
private static final String CONFIG_FILE_NAME = "system.cfg";
// 唯一的实例
private static volatile ConfigManager INSTANCE;
/**
* 将配置读取到内存
*/
private final Properties configs;
private ConfigManager() {
System.out.println("实例化配置管理器");
configs = new Properties();
try {
configs.load(ConfigManager.class.getClassLoader().getResourceAsStream(CONFIG_FILE_NAME));
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* 取得配置文件名
* @return
*/
public static String getConfigFileName() {
return CONFIG_FILE_NAME;
}
/**
* 取得实例
* @return
*/
public static ConfigManager getInstance() {
if (INSTANCE == null) {// step1
synchronized (ConfigManager.class) {// step2
if (INSTANCE == null) {// step3
INSTANCE = new ConfigManager();// OKK
}
}
}
return INSTANCE;
}
/**
* 取得配置
* @param configKey
* @return
*/
public String getConfigValue(String configKey) {
return configs.getProperty(configKey);
}
}
/**
* 实例保持的单例配置管理器
*/
public class ConfigManager {
// 配置文件
private static final String CONFIG_FILE_NAME = "system.cfg";
private static class InstanceHolder {
// 唯一的实例
private static ConfigManager INSTANCE = new ConfigManager();
}
/**
* 将配置读取到内存
*/
private final Properties configs;
private ConfigManager() {
System.out.println("实例化配置管理器");
configs = new Properties();
try {
configs.load(ConfigManager.class.getClassLoader().getResourceAsStream(CONFIG_FILE_NAME));
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* 取得配置文件名
* @return
*/
public static String getConfigFileName() {
return CONFIG_FILE_NAME;
}
/**
* 取得实例
* @return
*/
public static ConfigManager getInstance() {
return InstanceHolder.INSTANCE;
}
/**
* 取得配置
* @param configKey
* @return
*/
public String getConfigValue(String configKey) {
return configs.getProperty(configKey);
}
}
DCL较为复杂容易出错。利用JAVA类加载特性也能做到一样的效果,类加载是同步的,并且只需要做一次。
/**
* 枚举发方式的单例配置管理器
*/
public enum ConfigManager {
INSTANCE,
;
// 配置文件
private static final String CONFIG_FILE_NAME = "system.cfg";
/**
* 将配置读取到内存
*/
private final Properties configs;
ConfigManager() {
System.out.println("实例化配置管理器");
configs = new Properties();
try {
configs.load(ConfigManager.class.getClassLoader().getResourceAsStream(CONFIG_FILE_NAME));
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* 取得配置文件名
* @return
*/
public static String getConfigFileName() {
return CONFIG_FILE_NAME;
}
/**
* 取得实例
* @return
*/
public static ConfigManager getInstance() {
return INSTANCE;
}
/**
* 取得配置
* @param configKey
* @return
*/
public String getConfigValue(String configKey) {
return configs.getProperty(configKey);
}
}
枚举方式的效果类似饿汉式。
非常感谢你花时间阅读本文章,本人水平有限,如果有什么说的不对的地方,欢迎指正。欢迎各位留言讨论,希望小伙伴们都能每天进步一点点。