💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 1. 单例模式介绍 ### 1.1 定义 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 ### 1.2 为什么要用单例模式呢? 在我们的系统中,有一些对象其实我们只需要一个,比如说: ***线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡*** 等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。 **简单来说使用单例模式可以带来下面几个好处:** 1. 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销; 2. 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。 ### 1.3 为什么不使用全局变量确保一个类只有一个实例呢? 我们知道全局变量分为 ***静态变量*** 和 ***实例变量***,静态变量也可以保证该类的实例只存在一个。 只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。 但是,如果说这个对象非常消耗资源,而且程序某次的执行中一直没用,这样就造成了资源的浪费。利用单例模式的话,我们就可以实现 ***在需要使用时才创建对象***,这样就避免了不必要的资源浪费。 不仅仅是因为这个原因,在程序中我们要尽量避免全局变量的使用,大量使用全局变量给程序的调试、维护等带来困难。 ***** ## 2 单例的模式的实现 通常单例模式在Java语言中,有两种构建方式: * 饿汉方式。指全局的单例实例在类装载时构建 * 懒汉方式。指全局的单例实例在第一次被使用时构建。 不管是那种创建方式,它们通常都存在下面几点相似处: * 单例类必须要有一个 private 访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化; * instance 成员变量和 uniqueInstance 方法必须是 static 的。 ### 2.1 饿汉方式(线程安全) ``` public class Singleton { //在静态初始化器中创建单例实例,这段代码保证了线程安全 private static Singleton uniqueInstance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return uniqueInstance; } } ``` 所谓 “饿汉方式” 就是说JVM在加载这个类时就马上创建此唯一的单例实例,不管你用不用,先创建了再说,如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。 ### 2.2 懒汉式(非线程安全和synchronized关键字线程安全版本 ) ``` public class Singleton { private static Singleton uniqueInstance; private Singleton (){ } //没有加入synchronized关键字的版本是线程不安全的 public static Singleton getInstance() { //判断当前单例是否已经存在,若存在则返回,不存在则再建立单例 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } } ``` 所谓 “饿汉方式” 就是说单例实例在第一次被使用时构建,而不是在JVM在加载这个类时就马上创建此唯一的单例实例。 但是上面这种方式很明显是线程不安全的,如果多个线程同时访问getInstance()方法时就会出现问题。如果想要保证线程安全,一种比较常见的方式就是在getInstance() 方法前加上synchronized关键字,如下: ``` public static synchronized Singleton getInstance() { if (instance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } ``` 我们知道synchronized关键字偏重量级锁。虽然在JavaSE1.6之后synchronized关键字进行了主要包括:为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升。 但是在程序中每次使用getInstance() 都要经过synchronized加锁这一层,这难免会增加getInstance()的方法的时间消费,而且还可能会发生阻塞。我们下面介绍到的 双重检查加锁版本 就是为了解决这个问题而存在的。 ### 2.3 懒汉式(双重检查加锁版本) 利用双重检查加锁(double-checked locking),首先检查是否实例已经创建,如果尚未创建,“才”进行同步。这样以来,只有一次同步,这正是我们想要的效果。 ``` public class Singleton { //volatile保证,当uniqueInstance变量被初始化成Singleton实例时, //多个线程可以正确处理uniqueInstance变量 private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getInstance() { //检查实例,如果不存在,就进入同步代码块 if (uniqueInstance == null) { //只有第一次才彻底执行这里的代码 synchronized(Singleton.class) { //进入同步代码块后,再检查一次,如果仍是null,才创建实例 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } } ``` 很明显,这种方式相比于使用synchronized关键字的方法,可以大大减少getInstance() 的时间消费。 我们上面使用到了volatile关键字来保证数据的可见性,uniqueInstance 采用 volatile 关键字修饰也是很有必要。 uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: 1. 为 uniqueInstance 分配内存空间 2. 初始化 uniqueInstance 3. 将 uniqueInstance 指向分配的内存地址 但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。 指令重排 在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有 初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 ### 2.4 懒汉式(登记式/静态内部类方式) 静态内部实现的单例是懒加载的且线程安全。 只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance(只有第一次使用这个单例的实例的时候才加载,同时不会有线程安全问题)。 ``` public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } } ``` ### 2.5 饿汉式(枚举方式) 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化,同时这种方式也是《Effective Java 》以及《Java与模式》的作者推荐的方式。 ``` public enum Singleton { //定义一个枚举的元素,它就是 Singleton 的一个实例 INSTANCE; public void doSomeThing() { System.out.println("枚举方法实现单例"); } } ``` 使用方法: ``` public class ESTest { public static void main(String[] args) { Singleton singleton = Singleton.INSTANCE; singleton.doSomeThing();//output:枚举方法实现单例 } } ``` 《Effective Java 中文版 第二版》 这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。 —-《Effective Java 中文版 第二版》 《Java与模式》 《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。 ### 2.6 实例场景之 - 获取jdbc连接 我们新增两个类,第一个是非单例的获取jdbc连接 ``` package com.mk.designDemo.singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class MDataSourceNoSingle { private static final Logger logger = LoggerFactory.getLogger(MDataSourceNoSingle.class); private static Connection connection = null; public Connection getConnection() { try { Class.forName("com.mysql.jdbc.Driver"); connection = DriverManager.getConnection("jdbc:mysql://******:3306/*****?characterEncoding=UTF-8", "root", "root"); logger.info("线程{}实例化connection", Thread.currentThread().getName()); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } return connection; } } ``` 第二个使用了单例模式 ``` package com.mk.designDemo.singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class MDataSourceSingle { private static final Logger logger = LoggerFactory.getLogger(MDataSourceSingle.class); private static Connection connection = null; static { try { Class.forName("com.mysql.jdbc.Driver"); connection = DriverManager.getConnection("jdbc:mysql://******:3306/*****?characterEncoding=UTF-8", "root", "root"); logger.info("线程{}实例化connection", Thread.currentThread().getName()); } catch (ClassNotFoundException | SQLException e) { logger.error(e.getMessage(), e); } } private MDataSourceSingle() { } public static Connection getConnection() { return connection; } } ``` 然后我们写了一个简单的controller,里面有两个接口,用来验证结果 ``` package com.mk.designDemo.controller; import com.mk.designDemo.singleton.MDataSourceNoSingle; import com.mk.designDemo.singleton.MDataSourceSingle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.sql.Connection; @RestController public class SingletonController { private static final Logger logger = LoggerFactory.getLogger(SingletonController.class); private final static String sql = "select * from t_trade_date limit 10"; @RequestMapping(path = "/noSingleton") public String noSingleton() { MDataSourceNoSingle mDataSourceNoSingle = new MDataSourceNoSingle(); Connection connection = mDataSourceNoSingle.getConnection(); doExecute(connection, sql); return "OK"; } @RequestMapping(path = "/singleton") public String singleton() { doExecute(MDataSourceSingle.getConnection(), sql); return "OK"; } private void doExecute(Connection connection, String sql) { logger.info("do execute sql:{}", sql); } } ``` 当我们调用三次非单例的接口时,查看日志输出如下,connection被实例化了多次: ``` 2019-07-09 15:47:31.966 [http-nio-80-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - GET "/web/noSingleton", parameters={} 2019-07-09 15:47:31.970 [http-nio-80-exec-1] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to public java.lang.String com.mk.designDemo.controller.SingletonController.noSingleton() 2019-07-09 15:47:32.633 [http-nio-80-exec-1] INFO c.m.d.singleton.MDataSourceNoSingle - 线程http-nio-80-exec-1实例化connection 2019-07-09 15:47:32.633 [http-nio-80-exec-1] INFO c.m.d.controller.SingletonController - do execute sql:select * from t_trade_date limit 10 2019-07-09 15:47:32.671 [http-nio-80-exec-1] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json] 2019-07-09 15:47:32.672 [http-nio-80-exec-1] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing ["OK"] 2019-07-09 15:47:32.693 [http-nio-80-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK 2019-07-09 15:47:40.005 [http-nio-80-exec-4] DEBUG o.s.web.servlet.DispatcherServlet - GET "/web/noSingleton", parameters={} 2019-07-09 15:47:40.007 [http-nio-80-exec-4] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to public java.lang.String com.mk.designDemo.controller.SingletonController.noSingleton() 2019-07-09 15:47:40.361 [http-nio-80-exec-4] INFO c.m.d.singleton.MDataSourceNoSingle - 线程http-nio-80-exec-4实例化connection 2019-07-09 15:47:40.361 [http-nio-80-exec-4] INFO c.m.d.controller.SingletonController - do execute sql:select * from t_trade_date limit 10 2019-07-09 15:47:40.362 [http-nio-80-exec-4] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json] 2019-07-09 15:47:40.362 [http-nio-80-exec-4] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing ["OK"] 2019-07-09 15:47:40.363 [http-nio-80-exec-4] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK 2019-07-09 15:47:48.459 [http-nio-80-exec-7] DEBUG o.s.web.servlet.DispatcherServlet - GET "/web/noSingleton", parameters={} 2019-07-09 15:47:48.461 [http-nio-80-exec-7] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to public java.lang.String com.mk.designDemo.controller.SingletonController.noSingleton() 2019-07-09 15:47:48.801 [http-nio-80-exec-7] INFO c.m.d.singleton.MDataSourceNoSingle - 线程http-nio-80-exec-7实例化connection 2019-07-09 15:47:48.801 [http-nio-80-exec-7] INFO c.m.d.controller.SingletonController - do execute sql:select * from t_trade_date limit 10 2019-07-09 15:47:48.802 [http-nio-80-exec-7] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json] 2019-07-09 15:47:48.802 [http-nio-80-exec-7] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing ["OK"] 2019-07-09 15:47:48.803 [http-nio-80-exec-7] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK ``` 然后我们调用单例的接口三次,日志输出如下,我们可以看到,只有第一次进行了实例化,后面再也没有继续实例化 ``` 2019-07-09 15:49:22.868 [http-nio-80-exec-10] DEBUG o.s.web.servlet.DispatcherServlet - GET "/web/singleton", parameters={} 2019-07-09 15:49:22.871 [http-nio-80-exec-10] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to public java.lang.String com.mk.designDemo.controller.SingletonController.singleton() 2019-07-09 15:49:23.234 [http-nio-80-exec-10] INFO c.m.d.singleton.MDataSourceSingle - 线程http-nio-80-exec-10实例化connection 2019-07-09 15:49:23.235 [http-nio-80-exec-10] INFO c.m.d.controller.SingletonController - do execute sql:select * from t_trade_date limit 10 2019-07-09 15:49:23.237 [http-nio-80-exec-10] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json] 2019-07-09 15:49:23.237 [http-nio-80-exec-10] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing ["OK"] 2019-07-09 15:49:23.239 [http-nio-80-exec-10] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK 2019-07-09 15:49:25.032 [http-nio-80-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - GET "/web/singleton", parameters={} 2019-07-09 15:49:25.034 [http-nio-80-exec-1] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to public java.lang.String com.mk.designDemo.controller.SingletonController.singleton() 2019-07-09 15:49:25.034 [http-nio-80-exec-1] INFO c.m.d.controller.SingletonController - do execute sql:select * from t_trade_date limit 10 2019-07-09 15:49:25.035 [http-nio-80-exec-1] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json] 2019-07-09 15:49:25.036 [http-nio-80-exec-1] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing ["OK"] 2019-07-09 15:49:25.038 [http-nio-80-exec-1] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK 2019-07-09 15:49:25.926 [http-nio-80-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - GET "/web/singleton", parameters={} 2019-07-09 15:49:25.926 [http-nio-80-exec-2] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped to public java.lang.String com.mk.designDemo.controller.SingletonController.singleton() 2019-07-09 15:49:25.927 [http-nio-80-exec-2] INFO c.m.d.controller.SingletonController - do execute sql:select * from t_trade_date limit 10 2019-07-09 15:49:25.928 [http-nio-80-exec-2] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json] 2019-07-09 15:49:25.928 [http-nio-80-exec-2] DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor - Writing ["OK"] 2019-07-09 15:49:25.929 [http-nio-80-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Completed 200 OK ``` 验证通过,OVER! ### 2.7 总结 我们主要介绍到了以下几种方式实现单例模式: * 饿汉方式(线程安全) * 懒汉式(非线程安全和synchronized关键字线程安全版本) * 懒汉式(双重检查加锁版本) * 懒汉式(登记式/静态内部类方式) * 饿汉式(枚举方式) 参考: 《Head First 设计模式》 《Effective Java 中文版 第二版》 [【Java】设计模式:深入理解单例模式](https://itimetraveler.github.io/2016/09/08/%E3%80%90Java%E3%80%91%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%EF%BC%9A%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/#6、枚举)