一,简介
单例模式是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 < 30; i++) {
new Thread(()->{
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 < 400; i++) {
new Thread(()->{
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
**/
//声明他是一个配置类
@Configuration
public 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只能存在一个,通常有一个思路
①构造器私有
②本类创建实例
③将单实例暴漏给外界供外界使用
此文是作者纯手工打造,方便大家学习,转载请注明出处!