JAVA 设计模式,单例模式

752 阅读7分钟
  • 非常感谢你阅读本文,欢迎【👍点赞】【⭐收藏】【📝评论】~
  • 放弃不难,但坚持一定很酷!希望我们大家都能每天进步一点点!🎉

为什么要有设计模式?

一个有价值的系统总是会因为需求的变化而变化,可能是原有需求的修改,也可能是新需求的增加。于是可怜的猿们就得修改原来的代码。好的架构和设计可以让我们的代码结构具有良好的扩展性,在满足需求变化的同时仅需要修改尽可能少的代码,可以将需求变化对原系统的影响降到很低。设计模式就是人们对于良性架构设计的经验总结。


什么是单例模式?

单例模式的特点主要是:一是某个类只能有一个实例;二是这个类必须自己创建这个实例;三是它必须自行向整个系统提供这个实例。


为什么要有单例模式?

有时候需要一个类为系统提供服务,它通常不是为某个特定模块服务,而是整个系统多个地方可能都会需要这个服务。 最常见的就是系统配置,我们会把配置放在一个配置文件,有多个模块都会读取。为了资源的不浪费,这个类不应该有多个实例,因为这个类的表现只和那个配置文件有关,而配置文件只有一个,多个实例会浪费资源。那么这个唯一的实例到底谁去创建?思考一下会发现,对于使用这个配置功能的角色来说,他们需要得到实例,但是不能直接去实例化,因为那样就不能保证只有一个实例了。还必须看是否已经有一个实例了,如果有就直接用,没有就需要创建一个。但是这些事情每个客户端角色都去自己判断?最合理的方式就是这个配置类自己管理自己的实例,并提供取得这个实例的方法。这些需求刚好就是单例模式的特点。


单例模式的常用实现方式

饿汉式

配置文件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方法,造成实例化多次,必须将该方法声明为同步方法,这是个大的性能损失。


单例模式的其他实现方式

双重检查加锁double checked locking

懒汉式的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);
	}
}

实例持有方式Holder

/**
 * 实例保持的单例配置管理器
 */
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);
	}
}

枚举方式的效果类似饿汉式。