设计模式之单例模式

前言

上周刚立了个24年的flag,其中包括每周发一篇技术博客。其实这些年自己也没有好好系统的学习和整理过自己技术这块的知识体系,这些年长期在二线做技术管理,说实话具体编码其实也快忘完了。但有过和我一样经历的人大多有和我一样的情感,那就不甘心啊。总感觉想写点什么,比如博客,比如网站,比如app,比如小游戏等,最终因为各种原因维持在原地或者写了一篇就不写了。归结原因大多就是一下四种情况:

1、某段时间空一些,有了这个想法,然后后面就开始忙了,再回头看就放弃了;

2、给自己制定了太严格的计划,比如每周一篇,突然有一周没有坚持没写,就觉得不完美了;

3、好不容易写了一篇,正自鸣得意,一看网上大量写的比自己好的文章,默默地删除了;

4、不是以上三种,就是一时兴起异或三分钟热度异或不够持久(没开车);

别问我怎么总结出以上四种情况的,因为tmd 11年职业生涯里,上面四种我都经历过。不过,这次我又觉得我行了,所以立了个flag继续躁,失败了没什么损失不是吗?

后续,自己技术博客无法就两个方向。一种是项目中遇到的技术上的问题,解决后整理成文章。另一种,就是自己花了上万的网课,想想心疼一点点看完,之后总结成技术博客。

今天就从老生常谈的设计模式中的单例模式开篇。

什么是单例

单例模式我所理解就是一个系统只能有一份实例,无论你怎么去获取这个类中的实例,获取后都是同一份。

应用场景

根据单例模式的特性,经常被应用到一些共享数据场景内,比如系统初始化的全年局配置、线程间共享的实例对象。

以系统初始化的全年局配置为例,如果不使用单例,可能存在不同的开发人员在编程的时候自己去new一个全局配置实例用于读取全局配置。如果全局配置是完全静态的不会变更的,那最多只是浪费内存、增加垃圾回收的压力,只要资源够一般不会有什么大问题。当然,如果这个时候全局配置是可以进行修改的情况下,问题就来了。如果修改后的全局配置实例并没还有将数据写会配置文件或在其他全年局配置读取后再写会配置文件,那么意味着使用两个不同全局配置实例的功能存在配置不一致,最终可能导致线上事故。

以上举例只是说明多处功能需要使用一个实例的时候,确保无论谁更新了这个实例,其他的人都能读取到最新的配置,但是部分单例模式的写法需要增加一些多线程安全的代码。

当你在程序共享的数据对象比较大时,单例也有节省内存空间的作用,比如你的全局配置加载后几个G。

单例模式写法

孔乙己说茴香豆的茴有三种写法,单例模式目前比较主流的有8中写法,其中只有5中是没有线程安全问题的。当然还有人说有12中写法,饿好吧,我确实不知道。个人认为记住四种就行了,分别饿汉模式、懒汉模式、内部匿名类模式和枚举模式,当然记住他们这么写不重要,重要的是记住他们如何应用什么时候用。

饿汉模式

以下是直接实例化一个静态成员变量,其变种就是将实例化放到静态代码块中,结果一样。这两种这里就只写一种了,因为意义不大。

java 复制代码
/**
* @author xukr
* @version 1.0
* @description 饿汉模式 -- 推荐用法
* 类加载到内存中,通过实例化一个单例,jvm保证线程安全
* 优点:简单使用
* 缺点:提前占用内存
* @datetime 2023/12/4 3:18 PM
*/
public class Singleton1 {
   private static final Singleton1 INSTANCE = new Singleton1();
​
   private Singleton1(){}
​
   public static Singleton1 getINSTANCE() {
       return INSTANCE;
  }
​
   private void print(){
       System.out.println("单例饿汉模式1~");
  }
​
   public static void main(String[] args) {
       Singleton1 s1 = Singleton1.getINSTANCE();
       Singleton1 s2 = Singleton1.getINSTANCE();
       System.out.println(s2 == s1);
       Singleton1.getINSTANCE().print();
  }
}

懒汉模式

懒汉模式中有两种演进过程中的写法,但是都有协程安全问题,有兴趣可以自己了解一下。

懒汉1,线程安全效率低

csharp 复制代码
package dp.singleton;
​
/**
* @author xukr
* @version 1.0
* @description 懒汉模式 -- 改进版
* 类加载到内存中,通过实例化一个单例,jvm保证线程安全
* 优点:安全
* 缺点:效率低
* @datetime 2023/12/4 3:18 PM
*/
public class Singleton4 {
   private static volatile Singleton4 INSTANCE;
​
   private Singleton4(){}
​
   public static synchronized Singleton4 getINSTANCE() {
       if(INSTANCE == null){
           //增加睡一秒放大多线程的问题
           try {
               Thread.sleep(1000);
          } catch (InterruptedException e) {
               throw new RuntimeException(e);
          }
           INSTANCE = new Singleton4();
      }
       return INSTANCE;
  }
​
   private void print(){
       System.out.println("单例饿汉模式~");
  }
​
   public static void main(String[] args) {
       //验证线程安全问题
       for (int i = 0; i < 100; i++) {
           //hashcode相等不等于对象就相同
           new Thread(()-> System.out.println(Singleton4.getINSTANCE().hashCode())).start();
      }
  }
}

懒汉2,相对懒汉1,效率提高

csharp 复制代码
package dp.singleton;
​
/**
* @author xukr
* @version 1.0
* @description 懒汉模式 -- 双重检查
* 类加载到内存中,通过实例化一个单例,jvm保证线程安全
* 优点:安全
* 缺点:有线程安全问题
* @datetime 2023/12/4 3:18 PM
*/
public class Singleton6 {
   /**
    * 加上volatile繁殖JIT优化出现问题
    */
   private static volatile Singleton6 INSTANCE;
​
   private Singleton6(){}
​
   public static Singleton6 getINSTANCE() {
       if(INSTANCE == null){
           synchronized(Singleton6.class){
               if(INSTANCE == null){
                   //增加睡一秒放大多线程的问题
                   try {
                       Thread.sleep(1000);
                  } catch (InterruptedException e) {
                       throw new RuntimeException(e);
                  }
                   INSTANCE = new Singleton6();
              }
          }
      }
       return INSTANCE;
  }
​
   private void print(){
       System.out.println("单例饿汉模式~");
  }
​
   public static void main(String[] args) {
       //验证线程安全问题
       for (int i = 0; i < 100; i++) {
           //hashcode相等不等于对象就相同
           new Thread(()-> System.out.println(Singleton6.getINSTANCE().hashCode())).start();
      }
  }
}

内部匿名类模式

java 复制代码
package dp.singleton;
​
/**
* @author xukr
* @version 1.0
* @description 静态匿名内部类模式
* 类加载到内存中,通过实例化一个单例,jvm保证线程安全
* 优点:安全,简化
* @datetime 2023/12/4 3:18 PM
*/
public class Singleton7 {
   private Singleton7(){}
​
   private static class Innner{
       private static final Singleton7 INSTANCE = new Singleton7();
  }
​
   public static Singleton7 getINSTANCE() {
       return Innner.INSTANCE;
  }
​
   private void print(){
       System.out.println("静态匿名内部类模式~");
  }
​
   public static void main(String[] args) {
       //验证线程安全问题
       for (int i = 0; i < 100; i++) {
           //hashcode相等不等于对象就相同
           new Thread(()-> System.out.println(Singleton7.getINSTANCE().hashCode())).start();
      }
  }
}

枚举模式

typescript 复制代码
package dp.singleton;
​
/**
* @author xukr
* @version 1.0
* @description 枚举模式
* 优点:不会被反序列化,其他的都会被反序列化
* @datetime 2023/12/4 3:54 PM
*/
public enum Singleton8 {
   /**
    * 单例模式
    */
   INSTANCE;
   private void print(){
       System.out.println("枚举模式~");
  }
​
   public static void main(String[] args) {
       //验证线程安全问题
       for (int i = 0; i < 100; i++) {
           //hashcode相等不等于对象就相同
           new Thread(()-> System.out.println(Singleton8.INSTANCE.hashCode())).start();
      }
  }
}

以上,五种单例模式的写法饿汉模式和内部匿名类模式,都借助了jvm来保证线程安全,都是会用了static关键词,在类加载的时候实例化了单例。而两种懒汉模式需要我们自己实现线程安全保证,但他们不提前占用内存。

最后一种枚举类方式,因为其只有在使用其常量或方法或values()才会触发类的加载,所以不会提前暂用内存(内部静态成员变量和方法还是会被提前加载)拥有懒汉模式节省内存的优点。同时又枚举类的构造函数是私有的,无法在枚举类外部创建新的实例。这就保证了枚举类的实例都是唯一的、不可变的,并且具有相同的状态和行为。也就是说枚举类是天生的单例模式,而且他还拥有无法被反序列化的有点。也就是无法通过反射获取类,然后通过调用构造器创建新的实例。

总结

八种写法我只列举了5种,这五种都是可以放心使用,没有线程安全问题。其中最优的写法是枚举模式,因为在各方面他都最优。推荐饿汉模式,因为他最简单。但是大家都知道没有什么方法和工具是万能的,你了解其特性和优缺点就是为了在合适的地方使用合适的方法或工具。使用合适的工具和方法去做合适的事,这应该也是一个初级和中级的区别吧。本人能力有限水平一般,如本文有描述错误的地方,还望批评指正,在此感谢~

下一篇:设计模式之策略模式。

相关推荐
浮游本尊5 分钟前
Java学习第22天 - 云原生与容器化
java
Rexi9 分钟前
“Controller→Service→DAO”三层架构
后端
bobz96530 分钟前
计算虚拟化的设计
后端
深圳蔓延科技37 分钟前
Kafka的高性能之路
后端·kafka
Barcke39 分钟前
深入浅出 Spring WebFlux:从核心原理到深度实战
后端
JuiceFS39 分钟前
从 MLPerf Storage v2.0 看 AI 训练中的存储性能与扩展能力
运维·后端
大鸡腿同学41 分钟前
Think with a farmer's mindset
后端
Moonbit1 小时前
用MoonBit开发一个C编译器
后端·编程语言·编译器
Reboot2 小时前
达梦数据库GROUP BY报错解决方法
后端
稻草人22222 小时前
java Excel 导出 ,如何实现八倍效率优化,以及代码分层,方法封装
后端·架构