单例模式笔记

A blog for 单例模式-Study

Posted by if on 2021-09-20
Estimated Reading Time 5 Minutes
Words 1.3k In Total

单例模式笔记

前言

单例模式是指:确保一个类在任何情况下都绝对只有一个实例,隐藏其构造的方法,并提供一个全局访问点

例如ServletContext、ServletConfig、ApplicationContext、DBPool

你能记起多少单例?

饿汉式,饱汉式,双重检查式,静态内部类式

饿汉式

缺点:当需要加载的单例对象数量过多时,会造成内存浪费

==注:==如果是变成static代码块去new也是没有本质区别的——因为都是一开始就初始化了,不过至少可以放上对应的初始化的操作,还是比原生的要好一点点的

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @Author if
* @Description: 饿汉式单例:先创建好了,随时可以等着使用
* 线程安全,效率高,但是不能延迟加载
* @Date 2021-09-20 下午 08:19
*/
public class DanLiDemo01 {
private static DanLiDemo01 instance=new DanLiDemo01();
private DanLiDemo01() {}
public static DanLiDemo01 getInstance() {
return instance;
}
}

饱汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Author if
* @Description: 饱汉式单例:等待调用,调用里判断是否存在,不存在就创建
* 线程安全,效率较低,每次需要判断,且为了安全性需要加同步锁
* 但尽管这样做到了线程安全,并且解决了多实例问题,但并不高效
* 所以我们为此添加一个优化类————双重检查单例
* @Date 2021-09-20 下午 08:22
*/
public class DanLiDemo02 {
private static DanLiDemo02 instance=null;
private DanLiDemo02() {}
public synchronized static DanLiDemo02 getInstance(){
if(instance==null){
instance=new DanLiDemo02();
}
return instance;
}
}

双重检查式

参考链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* @Author if
* @Description: 双重检查单例
* @Date 2021-09-20 下午 08:45
*/
public class DanLiDemo04 {
/**
* 在早期的JVM中,synchronized(甚至是无竞争的synchronized)存在着巨大性能开销。
* 因此,出现双重检查锁定。以下是示例代码。
* 将饱汉式优化为如下的双重检查类(此方法有问题)
*
* 如上面的代码所示,如果第一次检查instance不为null,那就不需要执行下面的加锁和初始化操作。
* 因此,可以大幅降低synchronized带来的性能开销。
* 这样似乎很完美,但这是一个错误的优化!
* 在线程执行到第3行,代码读取到instance不为null时,instance引用的对象可能还没有完成初始化。
*/
private static DanLiDemo04 instance;

public static DanLiDemo04 getInstance(){
if(instance ==null) {
synchronized (DanLiDemo04.class) {
if (instance == null) {
instance = new DanLiDemo04();
}
}
}
return instance;
}

private static void doSomething(){

}
/**
* 那如何进行优化呢?
* 首先,先看一段代码
* 这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法
* 每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候
* 所以线程1会将stop变量的值拷贝一份放在自己的工作内存当中
* 那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了
* 那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去
*
* 但是用volatile修饰之后就变得不一样了
* 1.使用volatile关键字会强制将修改的值立即写入主存
* 2.使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效
* (反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)
* 3.由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取
* 即线程2修改了stop的值后,线程1读取不到缓存的值,会需要去主存读取,此时stop为true,线程1停止
*/
public static void main(String[] args) {
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
}
/**
* 基于volatile关键词的优化后的双重检查单例,即把实例声明为volatile
* 当instance2被改变后,立即消除线程工作内存的缓存,使其读取主存的地址
* 当然使用静态内部类单例更为简洁和方便
*/
private volatile static DanLiDemo04 instance2;
private DanLiDemo04() {}
public static DanLiDemo04 getInstance2(){
if(instance ==null) {
synchronized (DanLiDemo04.class) {
if (instance == null) {
instance = new DanLiDemo04();
}
}
}
return instance;
}
}

静态内部类式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @Author if
* @Description: 静态内部类单例模式
* 线程安全,效率高,只在调用时创建且不需要进行判断
* 与饿汉、饱汉的区别:
* 外部类没有static属性,所以不会像饿汉式一样立即创建对象
* 只有在调用到getInstance时才会加载内部类Instance,才会进行创建对象并返回
* 对象是static final的,所以保证了内存地址中只会有一个实例对象,保证了安全性
* 兼备了并发高效调用和延迟加载的优势
* @Date 2021-09-20 下午 08:24
*/
public class DanLiDemo03 {
private static class Instance{
public static final DanLiDemo03 instance=new DanLiDemo03();
}
private DanLiDemo03() {}
public static DanLiDemo03 getInstance(){
return Instance.instance;
}
}

本个人博客提供的内容仅用于个人学习,不保证内容的正确性。通过使用本站内容随之而来的风险与本站无关!