设计模式------单例模式
一,简介
单例模式是23种设计模式里面最简单的一个设计模式,该模式保证我们的bean对象,从始至终只有一个对象,它提供了创建对象的一种最佳方式,哪里需要用到此单例对象直接拿过来使用就可以,由于他自始至终只有一个对象,因此他节省了我们的内存空间,可以避免咱们的程序创建大量的对象,进而产生内存溢出的情况.
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态。
二,特点
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例.
- 单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性
三,单例的四大原则
- 构造私有
- 以静态方法或者枚举返回实例
- 确保实例只有一个,尤其是多线程环境
- 确保反序列换时不会重新构建对象
四,实现方式
实现单例模式有多种方式,根据他们的特点又可以分为延迟加载和立即加载,立即加载我们又叫做饿汉式, 延迟加载我们有叫做懒汉式,这种方式只有我们使用到的时候才会加载他.下面就来跟随小编的步伐一一实现他吧.
1.饿汉式
饿汉式顾名思义,我立刻就要到的我的对象来使用它,饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。
代码实现
/** * @program: Spring-Study * @description:饿汉式单例模式 * @author: Mr ZHAN * @create: 2022-05-24 16:27 **/public class HungrySingleton { //②创建完整对象 private static HungrySingleton hungry=new HungrySingleton(); //①构造器私有 private HungrySingleton() { } //③向外界暴露 public static HungrySingleton getInstance() { return hungry; }}
测试:
为了证明我们的bean对象是线程安全的,我们采用普通测试和启用多线程进行测试
/** * @program: Spring-Study * @description:饿汉式单例模式 * @author: Mr ZHAN * @create: 2022-05-24 16:27 **/public class HungrySingletonTest { public static void main(String[] args) { //单个测试 HungrySingleton instanceA = HungrySingleton.getInstance(); HungrySingleton instanceB = HungrySingleton.getInstance(); System.out.println("instanceA = " + instanceA);//com.dahai.singlton.hungry.HungrySingleton@71bc1ae4 System.out.println("instanceB = " + instanceB);//com.dahai.singlton.hungry.HungrySingleton@71bc1ae4 //启用多线程测试 for (int i = 0; i { HungrySingleton instance = HungrySingleton.getInstance(); System.out.println("instance = " + instance); }).start(); } }}
经过测试发现无论我们采用那种方式进行测试,他的地址值都是同一个.
2.饱汉式
饱汉式指的是只有我们在使用对象的时候才会去加载,如上面所示,我就坐着不动,我啥时候饿了,啥时候去吃饭,他与饿汉式不一样的是,饿汉式是主动出击,立马加载
代码实现
/** * @program: Spring-Study * @description:饱汉式-单例模式 * @author: Mr ZHAN * @create: 2022-05-24 17:45 **/public class FullSingleton { //②创建完整对象 private static FullSingleton full; //①构造器私有 private FullSingleton() { } //③向外界暴露 public static FullSingleton getInstance() { if (full==null){ full=new FullSingleton(); } return full; }}
测试:同样采用两种方式测试
/** * @program: Spring-Study * @description:饱汉式-单例模式 * @author: Mr ZHAN * @create: 2022-05-24 17:45 **/public class FullSingletonTest { public static void main(String[] args) { //普通测试 HungrySingleton instanceA = HungrySingleton.getInstance(); HungrySingleton instanceB = HungrySingleton.getInstance(); System.out.println("instanceA = " + instanceA);//com.dahai.singlton.hungry.HungrySingleton@71bc1ae4 System.out.println("instanceB = " + instanceB);//com.dahai.singlton.hungry.HungrySingleton@71bc1ae4 //启用多线程测试 for (int i = 0; i { FullSingleton instance = FullSingleton.getInstance(); System.out.println("instance = " + instance); }).start(); } }}
多线程测试结果:
根据实际的测试情况来看,不难发现,在非多线程的情况下,所创建的对象能够保持单例,但是在多线程的情况下出现了地址值不一样的情况,也就意味着产生了多个不同的对象,饱汉式在单线程的情况下能够正常保持对象是单例的,但是在多线程就未必了,因此我们采用同步锁来进行优化
3.饱汉式优化一
在方法上加synchronized同步锁或是用同步代码块对类加同步锁
代码实现
/** * @program: Spring-Study * @description:饱汉式优化一-单例模式 * @author: Mr ZHAN * @create: 2022-05-24 17:45 **/public class FullSingleton { //②创建完整对象 private static FullSingleton full; //①构造器私有 private FullSingleton() { } //③向外界暴露 public static synchronized FullSingleton getInstance() { if (full==null){ full=new FullSingleton(); } return full; }}
扩展:synchronized关键字是一把本地同步锁,可以自动的解锁,加锁,方法的同步是隐式的,代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。通过javap命令可以看到,他可以将关键字放在实例方法上,静态方法上以及同步代码块来保证线程的同步.但他们的同步锁却不相同
①实例方法:锁对象为this关键字(当前对象)
②静态方法:锁对象为类.class
③同步代码块:锁对象为类.class
此种方式虽然解决了多个实例对象问题,但是该方式运行效率却很低下,下一个线程想要获取对象,就必须等待上一个线程释放锁之后,才可以继续运行,因此我们可以采用双重检查锁来进行优化,使获取实例对象的方法仅对需要的请求有效.
4.饱汉式优化二(提高同步锁的效率)
使用双重检查锁,可以避免整个方法被锁,使得我们的方法只对需要的实例提供对象,提高同步锁的效率
代码实现
package com.dahai.singlton.full;import com.dahai.singlton.hungry.HungrySingleton;/** * @program: Spring-Study * @description:饱汉式优化二-单例模式 * @author: Mr ZHAN * @create: 2022-05-24 17:45 **/public class FullSingleton { //②创建完整对象 private static volatile FullSingleton full; //①构造器私有 private FullSingleton() { } //③向外界暴露 public static FullSingleton getInstance() { if (full==null){ synchronized (FullSingleton.class) { if (full==null){ full=new FullSingleton(); } } } return full; }}
同样的方法去测试,可以保证咱们的对象都为单例,细心的朋友可能会发现我在此代码中使用了volatile关键字由于咱们的jmm内存模型分为核心内存和工作内存两部分,每一个线程会在自己的工作内存里面完成,于是有了这个关键字,他可以保证可见性,不保证原子性,禁止指令重排,此处使用他的目的是为了防止 创建对象时的指令重排问题,导致其他线程使用对象时造成空指针问题
5.静态内部类实现
这种方式引入了一个内部静态类(static class),静态内部类只有在调用时才会加载,它保证了Singleton 实例的延迟初始化,又保证了实例的唯一性。它把singleton 的实例化操作放到一个静态内部类中,在第一次调用getInstance() 方法时,JVM才会去加载InnerObject类,同时初始化singleton 实例,所以能让getInstance() 方法线程安全。
特点是:即能延迟加载,也能保证线程安全。
/** * @program: Spring-Study * @description:静态内部类-单例模式 * @author: Mr ZHAN * @create: 2022-05-24 17:45 **/public class StaticSingleton { //①构造器私有 private StaticSingleton() { } //②静态内部类延迟加载 public static class InnerObject{ private static StaticSingleton staticSingleton=new StaticSingleton(); } //向外暴露 public static StaticSingleton getInstance(){ return InnerObject.staticSingleton; }}
同样采用多线程测试,可以看到获得的线程都是单例的,静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。
6.枚举类实现(防止反射和反序列化攻击)
事实上,通过Java反射机制是能够实例化构造方法为private的类的。这也就是我们现在需要引入的枚举单例模式。
6.public class SingletonFactory { 7. 8. /** 9. * 内部枚举类 10. */ 11. private enum EnumSingleton{ 12. Singleton; 13. private Singleton6 singleton; 14. 15. //枚举类的构造方法在类加载是被实例化 16. private EnumSingleton(){ 17. singleton = new Singleton6(); 18. } 19. public Singleton6 getInstance(){ 20. return singleton; 21. } 22. } 23. 24. public static Singleton6 getInstance() { 25. return EnumSingleton.Singleton.getInstance(); 26. } 27.} 28. 29.class Singleton6 { 30. public Singleton6(){} 31.}
五,常用项目使用
通常情况下我们使用线程池,需要使线程池对象为单例的,一般情况下我们在spring下开发,由于spring的对象默认为单列,因此我们可以直接将我们的线程池对象,交由我们的spring的ioc容器去管理,详情见以下代码
/** * @program: Spring-Study * @description: * @author: Mr ZHAN * @create: 2022-05-24 18:42 **///声明他是一个配置类@Configurationpublic class ThreadPoolApplication { /** * 创建线程池对象交由ioc * 七个核心参数 * ①核心线程数 * ②最大线程数 * ③存活时间 * ④单位 * ⑤等待队列 * ⑥线程工厂 * ⑦拒绝策略(4种 1.直接拒绝 2.交由调用者处理 3.丢弃等待时间最长 4.报错) * */ @Bean public ThreadPoolExecutor getThreadPoolExecutor(){ return new ThreadPoolExecutor(3, 6, 100, TimeUnit.SECONDS, new ArrayBlockingQueue(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); }}
如何选择线程数:
线程数的选择根据cpu的核数n来判断,如果程序为cpu密集型,cpu会一直工作,利用率非常高,通常设置为n+1,如果程序为io密集型,整个程序会发生io操作,整个系统cpu的利用率不是特别足,因此通常最大线程数设置为2n
六,总结
单例模式是最简单的一种设计模式,无外乎就是实例bean只能存在一个,通常有一个思路
①构造器私有
②本类创建实例
③将单实例暴漏给外界供外界使用
此文是作者纯手工打造,方便大家学习,转载请注明出处!