平常开发中,调用其他系统的接口是很常见的,调用一般需要用到一些配置信息,而这些配置信息一般在配置文件中,程序启动时读取到内存中使用。

例如有如下配置文件。

  1. # 文件名 ThirdApp.properties
  2. appId=188210
  3. secret=MIVD587A12FE7E

程序直接读取配置文件,解析将配置信息保存在一个对象中,调用其他系统接口时使用这个对象即可。

  1. package com.chenpi.singleton;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.util.Properties;
  5. /**
  6. * @Description 第三方系统相关配置信息
  7. * @Author 陈皮
  8. * @Date 2021/5/16
  9. * @Version 1.0
  10. */
  11. public class ThirdConfig {
  12. private String appId;
  13. private String secret;
  14. public ThirdConfig() {
  15. // 初始化
  16. init();
  17. }
  18. /**
  19. * 读取配置文件,进行初始化
  20. */
  21. private void init() {
  22. Properties p = new Properties();
  23. InputStream in = ThirdConfig.class.getResourceAsStream("ThirdApp.properties");
  24. try {
  25. p.load(in);
  26. this.appId = p.getProperty("appId");
  27. this.secret = p.getProperty("secret");
  28. } catch (IOException e) {
  29. e.printStackTrace();
  30. } finally {
  31. try {
  32. if (null != in) {
  33. in.close();
  34. }
  35. } catch (IOException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. }
  40. public String getAppId() {
  41. return appId;
  42. }
  43. public String getSecret() {
  44. return secret;
  45. }
  46. }

使用的时候,直接创建对象,进行使用。

  1. package com.chenpi.singleton;
  2. /**
  3. * @Description
  4. * @Author 陈皮
  5. * @Date 2021/5/16
  6. * @Version 1.0
  7. */
  8. public class ChenPiMain {
  9. public static void main(String[] args) {
  10. ThirdConfig thirdConfig = new ThirdConfig();
  11. System.out.println("appId=" + thirdConfig.getAppId());
  12. System.out.println("secret=" + thirdConfig.getSecret());
  13. }
  14. }
  15. // 输出结果如下
  16. appId=1007
  17. secret=陈皮的JavaLib

通过以上分析,需要使用配置信息时,只要获取 ThirdConfig 类的实例即可。但是如果项目是多人协作的,其他人使用这个配置信息的时候选择每次都去 new 一个新的实例,这样就会导致每次都会生成新的实例,并且重复读取配置文件的信息,多次 IO 操作,系统中存在多个 AppConfig 实例对象,严重浪费内存资源,如果配置文件内容很多的话,浪费系统资源更加严重。

针对以上问题,因为配置信息是不变的,共享的,所以我们只要保证系统运行期间,只有一个类实例存在就可以了。单例模式就用来解决类似这种问题的。

在系统运行期间,一个类仅有一个实例,并提供一个对外访问这个实例的全局访问点。

将类的构造方法私有化,只能类自身来负责创建实例,并且只能创建一个实例,然后提供一个对外访问这个实例的静态方法,这就是单例模式的实现方式。

在 Java 中,单例模式的实现一般分为两种,懒汉式和饿汉式,它们之间主要的区别是在创建实例的时机上,一种是提前创建实例,一种是使用时才创建实例。

饿汉式、顾名思义,很饥饿很着急,所以在类加载器装载类的时候就创建实例,由 JVM 保证线程安全,只创建一次,饿汉式实现示例代码如下:

  1. package com.chenpi.singleton;
  2. /**
  3. * @Description 饿汉式单例模式
  4. * @Author 陈皮
  5. * @Date 2021/5/16
  6. * @Version 1.0
  7. */
  8. public class HungerSingleton {
  9. /**
  10. * 提前实例化,由JVM保证实例化一次 使用static关键字修饰,使它在类加载器装载后,初始化此变量,并且能让静态方法getInstance使用
  11. */
  12. private static final HungerSingleton INSTANCE = new HungerSingleton();
  13. /**
  14. * 私有化构造方法,只能内部调用,外部调用不了则避免了多次实例化的问题
  15. */
  16. private HungerSingleton() {}
  17. /**
  18. * 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
  19. *
  20. * @return HungerSingleton 实例
  21. */
  22. public static HungerSingleton getInstance() {
  23. return INSTANCE;
  24. }
  25. }

懒汉式、顾名思义,既然懒就不着急,即等到要使用对象实例的时候才创建实例,更准确的说是延迟加载。懒汉式实现示例代码如下:

  1. package com.chenpi.singleton;
  2. /**
  3. * @Description 懒汉式单例模式
  4. * @Author 陈皮
  5. * @Date 2021/5/16
  6. * @Version 1.0
  7. */
  8. public class LazySingleton {
  9. /**
  10. * 存储创建好的类实例,赋值null,使用时才创建赋值 因为静态方法getInstance使用了此变量,所以使用static关键字修饰
  11. */
  12. private static LazySingleton instance = null;
  13. /**
  14. * 私有化构造方法,只能内部调用,外部调用不了则避免了多次实例化的问题
  15. */
  16. private LazySingleton() {}
  17. /**
  18. * 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
  19. *
  20. * @return LazySingleton 实例
  21. */
  22. public static LazySingleton getInstance() {
  23. // 判断实例是否存在
  24. if (instance == null) {
  25. instance = new LazySingleton();
  26. }
  27. // 如果已创建过,则直接使用
  28. return instance;
  29. }
  30. }

饿汉式,当类装载的时候就创建类实例,不管用不用,后续使用的时候直接获取即可,不需要判断是否已经创建,节省时间,典型的空间换时间。

懒汉式,等到使用的时候才创建类实例,每次获取实例都要进行判断是否已经创建,浪费时间,但是如果未使用前,则能达到节约内存空间的效果,典型的时间换空间。

从线程安全性上讲, 饿汉式是类加载器加载类到 JVM 内存中后,就实例化一个这个类的实例,由 JVM 保证了线程安全。而不加同步的懒汉式是线程不安全的,在并发的情况下可能会创建多个实例。

如果有两个线程 A 和 B,它们同时调用 getInstance 方法时,可能导致并发问题,如下:

  1. public static LazySingleton getInstance() {
  2. // 判断对象实例是否已被创建
  3. if (instance == null) {
  4. // 线程A运行到这里了,正准备创建实例,或者实例还未创建完,
  5. // 此时线程B判断instance还是为null,则线程B也进入此,
  6. // 最终线程A和B都会创建自己的实例,从而出现了多实例
  7. instance = new LazySingleton();
  8. }
  9. // 如果已创建过,则直接使用
  10. return instance;
  11. }

所以我们一般推荐饿汉式单例模式,因为由 JVM 实例化,保证了线程安全,实现简单。而且这个实例总会用到的时候,提前实例化准备好也未尝不可。

4.1 双重检查加锁

前面说到,懒汉式单例模式在并发情况下可能会出现线程安全问题,那我们可以通过加锁,保证只能一个线程去创建实例即可,只要加上 synchronized 即可,如下所示:

  1. public static synchronized LazySingleton getInstance() {
  2. // 判断对象实例是否已被创建
  3. if (instance == null) {
  4. // 第一次使用,没用被创建,则先创建对象,并且存储在类变量中
  5. instance = new LazySingleton();
  6. }
  7. // 如果已创建过,则直接使用
  8. return instance;
  9. }

如果对整个方法加锁,会降低访问性能,即每次都要获取锁,才能进入执行方法。可以使用双重检查加锁的方式来实现,就可以既实现线程安全,又能够使性能不受到很大的影响。

双重检查加锁机制:每次进入方法不需要同步,进入方法后,先检查实例是否存在,如果不存在才进入加锁的同步块,这是第一重检查。进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。

使用双重检查加锁机制时,需要借助 volatile 关键字,被它修饰的变量,变量的值就不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,所以能确保多个线程能正确的处理该变量。

  1. package com.chenpi.singleton;
  2. /**
  3. * @Description 懒汉式单例模式
  4. * @Author 陈皮
  5. * @Date 2021/5/16
  6. * @Version 1.0
  7. */
  8. public class LazySingleton {
  9. /**
  10. * 存储创建好的类实例,赋值null,使用时才创建赋值 因为静态方法getInstance使用了此变量,所以使用static关键字修饰
  11. */
  12. private volatile static LazySingleton instance = null;
  13. /**
  14. * 私有化构造方法,只能内部调用,外部调用不了则避免了多次实例化的问题
  15. */
  16. private LazySingleton() {
  17. }
  18. /**
  19. * 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
  20. *
  21. * @return LazySingleton 实例
  22. */
  23. public static synchronized LazySingleton getInstance() {
  24. // 第一重检查,判断实例是否存在,如果不存在则进入同步块
  25. if (instance == null) {
  26. // 同步块,能保证同时只能有一个线程访问
  27. synchronized (LazySingleton.class) {
  28. // 第二重检查,保证只创建一次对象实例
  29. if (instance == null) {
  30. instance = new LazySingleton();
  31. }
  32. }
  33. }
  34. // 如果已创建过,则直接使用
  35. return instance;
  36. }
  37. }

有一种方式,既有饿汉式的优点(线程安全),又有懒汉式的优点(延迟加载),那就是使用静态内部类。

何为静态内部类呢?即在类中定义的并且使用 static 修饰的类。可以认为静态内部类是外部类的静态成员,静态内部类对象与外部类对象间不存在依赖关系,因此可直接创建。

静态内部类中的静态方法可以使用外部类中的静态方法和静态变量;而且静态内部类只有在第一次被使用的时候才会被装载,达到了延迟加载的效果。然后我们在静态内部类中定义一个静态的外部类的对象,并进行初始化,由 JVM 保证线程安全,进行创建。

  1. package com.chenpi.singleton;
  2. /**
  3. * @Description 静态内部类实现单例模式
  4. * @Author Mr.nobody
  5. * @Date 2021/5/16
  6. * @Version 1.0
  7. */
  8. public class StaticInnerClassSingleton {
  9. /**
  10. * 静态内部类,外部内未使用内部类时,类加载器不会加载内部类
  11. */
  12. private static class SingletonHolder {
  13. /**
  14. * 静态初始化,由JVM保证线程安全
  15. */
  16. private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
  17. }
  18. /**
  19. * 私有化构造方法
  20. */
  21. private StaticInnerClassSingleton() {
  22. }
  23. /**
  24. * 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
  25. *
  26. * @return StaticInnerClassSingleton 实例
  27. */
  28. public static StaticInnerClassSingleton getInstance() {
  29. return SingletonHolder.INSTANCE;
  30. }
  31. }

还有一种单例模式的最佳实现,就是借助枚举。实现简洁,高效安全。没有构造方法,还能防止反序列化,并由 JVM 保障单例。实例代码如下:

  1. package com.chenpi.singleton;
  2. /**
  3. * @Description 枚举实现单例模式,没有构造方法,能防止反序列化
  4. * @Author 陈皮
  5. * @Date 2021/5/16
  6. * @Version 1.0
  7. */
  8. public enum EnumSingleton {
  9. /**
  10. * 定义一个枚举的元素,代表要实现类的一个实例
  11. */
  12. INSTANCE;
  13. // 可以定义方法
  14. public void test() {
  15. System.out.println("Hello ChenPi!");
  16. }
  17. }

如果要使用,直接使用即可,如下:

  1. package com.chenpi.singleton;
  2. /**
  3. * @Description
  4. * @Author 陈皮
  5. * @Date 2021/5/16
  6. * @Version 1.0
  7. */
  8. public class ChenPiMain {
  9. public static void main(String[] args) {
  10. EnumSingleton instance = EnumSingleton.INSTANCE;
  11. instance.test();
  12. }
  13. }
  14. // 输出结果
  15. Hello ChenPi!

单例模式,只有一个类实例。如果要求指定数量的类实例,例如指定2个或者3个,或者任意多个?

其实万变不离其宗,单例模式是只创建一个类实例,并且存储下来。那指定数量多例模式,就创建指定的数量类实例,也存储下来,使用时根据策略(例如轮询)取指定的实例即可。以下演示指定5个实例的情况:

  1. package com.chenpi.singleton;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. /**
  5. * @Description 指定数量的多实例模式
  6. * @Author 陈皮
  7. * @Date 2021/5/16
  8. * @Version 1.0
  9. */
  10. public class ExtendSingleton {
  11. /**
  12. * 指定的实例数
  13. */
  14. private final static int INSTANCE_NUM = 5;
  15. /**
  16. * key前缀
  17. */
  18. private final static String PREFIX_KEY = "instance_";
  19. /**
  20. * 缓存实例的容器
  21. */
  22. private static Map<String, ExtendSingleton> map = new HashMap<>();
  23. /**
  24. * 记录当前正在使用第几个实例
  25. */
  26. private static int num = 1;
  27. /**
  28. * 私有化构造方法,只能内部调用,外部调用不了则避免了多次实例化的问题
  29. */
  30. private ExtendSingleton() {
  31. }
  32. /**
  33. * 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
  34. *
  35. * @return ExtendSingleton 实例
  36. */
  37. public static ExtendSingleton getInstance() {
  38. // 缓存key
  39. String key = PREFIX_KEY + num;
  40. // 优先从缓存中取
  41. ExtendSingleton extendSingleton = map.get(key);
  42. // 如果指定key的实例不存在,则创建,并放入缓存容器中
  43. if (extendSingleton == null) {
  44. extendSingleton = new ExtendSingleton();
  45. map.put(key, extendSingleton);
  46. }
  47. // 当前实例的序号加1
  48. num++;
  49. if (num > INSTANCE_NUM) {
  50. // 实例的序号达到最大值重新从1开始
  51. num = 1;
  52. }
  53. return extendSingleton;
  54. }
  55. }

本次分享到此结束啦~~

如果觉得文章对你有帮助,点赞、收藏、关注、评论,您的支持就是我创作最大的动力!

我是陈皮,一个在互联网 Coding 的 ITer,微信搜索「陈皮的JavaLib」第一时间阅读最新文章。

版权声明:本文为luciochn原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/luciochn/p/15086911.html