当你写作业的时候,你肯定是想有个分身来帮你做作业,然后你就舒舒服服吃着薯片、喝着肥宅水看电视。但是等到你领工资的时候,你肯定不想自己分成几个人然后把你工资领了~(ˉ▽ˉ;)…或者,怎么确保你就是你,不是你的克隆人…
文章目录
前言
世界上没有两片完全一样的雪花
抛开程序不谈,先好好想想一下这个世界。世界上没有两片完全一样的雪花
,世上没有感同身受,只有冷暖自知
。是不是很神奇?o( ̄▽ ̄)ブ先辈的经历都告诉我们,这个世界无论是在物质上还是在精神上,每个物体或者个体都是独一无二的┑( ̄Д  ̄)┍。这种思想映射到程序就是一种设计思想,一种设计模式——单例模式
一、什么是单例模式?
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
而Java对单例模式的定义为:一个类有且仅有一个实例,并且自行实例化向整个系统提供。
用人话来说就是,当你的女神要交课程大作业的时候,你先看看自己有没有准备,如果已经准备了答案的话就直接给你的女神(饿汉模式),如果没有准备的话你就先写完然后再给你的女神(懒汉模式)。女神只要找到你就能拿到课程大作业答案~(什么是舔狗的自我修养இ௰இ)
二、什么是饿汉模式
作为一个卷王,即使我是一只卑微的舔狗,我也要成为舔狗中的卷王!等到我的女神们发现离不开我,等到其他舔狗都被我卷死,我就能成功上位(想桃子吃~)
1.概念
饿汉模式就是在类加载的时候迫不及待,立刻实例化。(相当于发布了课堂大作业就立刻写答案),后续使用就会直接调用这一份实例(就是说你的女神们问你都只会拿到这一份作业)。
2.代码展示
public class HungrySingle {
//在类加载的时候就实例化了,类加载只有一次,所以值实例化出了一份该实例对象
private static HungrySingle instance = new HungrySingle();
public static HungrySingle getInstance() {
return instance;
}
3.特点
- 私有类的构造器
- 提供该类的静态修饰的对象
- 提供公开的静态方法,返回该类的唯一对象
4.优缺点
- 优点:对象提前创建好了,使用的时候无需等待,效率高
- 缺点:对象提前创建,所以会占据一定的内存,内存占用大
以空间换时间
三、什么是懒汉模式
一昧的做舔狗是没出息的,纯纯工具人。我才不在一颗树上吊死,做那么多无用功简直浪费时间,大佬都是最后才登场的。女神以为我是她鱼塘里面的一条鱼,但是有没有一种可能,我也有很多女神,谁是谁鱼塘里面的一条鱼还说不定呢~(●’◡’●)
1.概念
懒汉模式就是说用的时候才会进行实例化,但是这种临时抱佛脚的做法会出现很多奇怪的情况~
2.代码展示
如果只有一个女神找你的话,代码应该是这样子的~
代码如下(示例):
public class LazySingle {
private static LazySingle instance = null;
//只有在调用该方法的时候才实例化
public static LazySingle getInstance() {
if(instance == null) {
instance = new LazySingle();
}
return instance;
}
}
问题来了,同时两个女神都来找你,你已经写的焦头烂额咯(ˉ▽ˉ;)…居然没发现是一样的,然后你又准备写一份作业/(ㄒoㄒ)/~~
简单来说,多线程环境下,多个线程同时到达这个方法,都认为没有实例化这个对象,然后就同时实例这个对象。直接导致生成两个相同的对象,违反了单例模式的规定。那该怎么做呢?说实话,可以狠心一点,一次只和一个女神聊天~在程序上就是加锁咯
public class LazySingle {
private static LazySingle instance = null;
//只有在调用该方法的时候才实例化
public static synchronized LazySingle getInstance() {
if(instance == null) {
instance = new LazySingle();
}
return instance;
}
}
其实这种方法已经很棒了,但是这样子抛弃其他女神,说实话真不行=,=。只能想其他方法咯~
加锁的方法虽然保证了线程安全,但是每次调用该方法都得加锁,加锁就会导致程序只能串行执行!加锁后就无法进行高并发的操作,为了提高程序的执行效率,还需要继续优化。
其实不用那么死板的嘛,虽然每次都只和一个女神聊天,但是每个女神找我,我都先看看这个作业做了没有=,=,做完就能直接给女神,这样子就不用重复做一样的作业对吧🤭(时间管理大师~)
双重
if
判断加锁提高效率,在Java中需要配合volatile
来实现
public class SecurityLazyModle {
private LazySingle() {
}
private static volatile SecurityLazyModle instance = null;//保证内存可见性,防止编译器过度优化(指令重排序)
public static SecurityLazyModle getInstance() {
if(instance == null) {
synchronized (SecurityLazyModle.class) {
if(instance == null) {
instance = new SecurityLazyModle();
}
}
}
return instance;
}
}
代码分析:第一层
if
是为了判断当前是否已经把实例创建出来,第二层synchronized
是为了使进入当前if中的线程来竞争锁,当拿到锁的线程进入到第三层if
之后判断是否为空,不为空就是实例化对象,然后再释放锁,释放锁之后,instance
已经不为空了,后面的线程就被阻挡在了第三层if这里了,之后再来访问getInstance()
方法,发现该instance
已经不为空了,也就不用再抢占锁资源了,因为竞争锁也消耗大量的时间。通过这样处理,既保证了线程安全,也提高了效率。
知识补充:
指令重排序:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能
这里使用volatile是为了防止编译器优化导致的指令重排序。值得注意的是:new
一个对象不是原子性操作,可以分为三步骤:
为对象分配内存空间
初始化对象
将对象指向分配好的内存空间
对于上面的执行,如果1和3先执行了(假设2还没有完成),在第一层
if
外的线程这时候判断不为null
,这时候就会直接返回该对象,但是这个对象还没进行初始化!之后使用就会导致线程安全问题。
通过volatile
就可以确保这3步骤必须执行完(无论顺序如何,最终都会执行完),外面的线程才可以执行,这时候就保证了该对象的完整性。
volatile
还有第二个作用:使用volatile
关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。
3、特点
- 私有构造方法
- 提供该类的静态变量,不是马上创建
- 提供公开的获取唯一对象的静态方法
4、优缺点
- 优点:使用对象时,对象才创建,所以不会提前占用内存,内存占用小
- 缺点:首次使用对象时,需要等待对象的创建,而且每次都需要判断对象是否为空,运行效率较低
以时间换空间
四、不讲武德的小伙子——反射和序列化
想象一下,有人直接把你作业偷掉,复制一份给女神,那你岂不是做了无用功?还真有这种不讲武德的小伙子——反射和序列化
反射
利用反射,强制访问类的私有构造器,去创建另一个对象
public static void main(String[] args) {
// 获取类的显式构造器
Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
// 可访问私有构造器
construct.setAccessible(true);
// 利用反射构造新对象
Singleton obj1 = construct.newInstance();
// 通过正常方式获取单例对象
Singleton obj2 = Singleton.getInstance();
System.out.println(obj1 == obj2); // false
}
序列化
public static void main(String[] args) {
// 创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// 将单例对象写到文件中
oos.writeObject(Singleton.getInstance());
// 从文件中读取单例对象
File file = new File("Singleton.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == Singleton.getInstance()); // false
}
那怎么防这两个老六呢?
五、居家必备好助手——枚举
这种方法仅适用jdk1.5之后
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("这是枚举类型的单例模式!");
}
}
- 优势1:代码对比饿汉式与懒汉式来说,更加地简洁
- 优势2:它不需要做任何额外的操作去保证对象单一性与线程安全性
- 优势3:使用枚举可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象,破坏单例模式。
六、应用场景
- 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 配置对象、数据库的连接池等。
- 当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
- Spring的IOC容器里面的Bean默认为单例模式。
总结
世界上没有两片完全一样的雪花,单例模式是一种比较朴素的思想,在程序设计上分为懒汉模式和饿汉模式还有枚举类,该模式在Spring等框架也广泛应用。