言成言成啊 | Kit Chen's Blog

理解单例模式

发布于2022-06-28 22:29:42,更新于2022-07-23 01:57:31,标签:java  文章会持续修订,转载请注明来源地址:https://meethigher.top/blog

最近,习大大要调研光谷企业,所以我也就调休了,然后总结下最近用到的单例!

说起来可笑,去年3月份面试时提到的DCL,不会。工作一年后,才略懂。纸上谈兵终觉浅,绝知此事要躬行!

单例模式,网上说是最简单的设计模式。

单例,顾名思义,单个实例,属于创建型模式,通过静态变量存储唯一的对象实例。我们new的对象,每new一个地址都不同,这叫做多例。

单例模式举个应用场景是,一个专用加解密对象,在创建时,通过一个随机密钥生成实例,我要保证所有使用的人,都拿到同一个加解密实例,才能解密别人加密过的。

下面举例单例模式的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton {
private static Singleton singleton;

private Singleton() {
//构造函数私有化,不允许在类外创建对象
try {
Thread.sleep(100L);//模拟创建对象耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s-创建对象\n", Thread.currentThread().getId());
}

public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}

这是最经典的单例模式写法,在普通环境中运行良好。但是在高并发环境中,可能会出现创建出多个对象实例的现象。参见如下测试代码:

1
2
3
4
5
public static void main(String[] args) {
for (int i=0;i<10;i++) {
new Thread(Singleton::getInstance).start();
}
}

出现如上情况的原因是:多个线程同时调用getInstance方法时,判断singleton==null时,都满足条件,因此会创建多个实例对象,这也叫做线程不安全问题。

一、如何保证单例

解决办法

  1. 加锁。给getInstance方法增加synchronized同步锁。但是,这种做法,在高并发下,虽然保证了只拿到一个实例对象,但是也让后续的并发拿取耗费了不少性能。
  2. 懒汉模式
  3. 恶汉模式

1.1 懒汉模式-懒加载

顾名思义,他是一个懒汉,他不愿意动弹。什么时候需要吃饭了,他就什么时候开始想办法搞点食物。

即懒汉式一开始不会实例化,什么时候用就什么时候new,才进行实例化。

懒汉模式,有两种可选方式。

  • 双检锁
  • 延迟初始化占位类

双检锁

通过双重校验锁(double-checked-locking),即双检锁(DCL)。既保证了不影响并发调用的性能,又确保了并发时只会被创建一次。

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
public class Singleton {
//在多线程操作singleton变量时,使用volatile保证线程改变的值立即回写主内存,保证其他线程能拿到最新的值
private volatile static Singleton singleton;

private Singleton() {
//构造函数私有化,不允许在类外创建对象
try {
Thread.sleep(100L);//模拟创建对象耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s-创建对象\n", Thread.currentThread().getId());
}

public static Singleton getInstance() {
//第一次检查
if (singleton == null) {
//加锁第二次检查
synchronized (Singleton.class) {
if(singleton==null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

延迟初始化占位类

延迟初始化占位类,算是对双检锁的一个优化,能做到同样的效果,主要是从理解程度上,更易于理解!

一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用、卸载七个阶段。这也是俗称整个类加载的生命周期

类加载与类加载生命周期,不是一回事。前者是后者的一部分。

加载阶段,书中没有明确提及。也正因为这个不明确,导致之后做开发时碰到了Bug

但是类的初始化阶段,是有严格规范的。其中,调用静态方法、静态字段时,就会触发初始化。初始化完成之后才能被使用,此处就做到了类似于同步锁的作用。

类的初始化与对象的实例化也需要分清。

new 对象可以触发该类的初始化和对象的实例化。

仅调用静态方法,只会触发类的初始化。

参考自《深入理解Java虚拟机》

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
public class Singleton {

private Singleton() {
//构造函数私有化,不允许在类外创建对象
try {
Thread.sleep(100L);//模拟创建对象耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s-创建对象\n", Thread.currentThread().getId());
}

/**
* 类都是有引用到的时候,才会被加载。
* 此处可以通过添加vmoptions参数-verbose:class,查类加载情况
*/
private static final class SingletonHolder {
private static final Singleton singleton = new Singleton();
}

/**
* 懒加载单例
*
* @return
*/
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}

1.2 饿汉模式-急加载

顾名思义,他是一个饿汉,他很勤快就怕自己饿着。他总是先把食物准备好,什么时候需要吃了,他随时拿来吃,不需要临时去搞食物。

即饿汉式在类加载的时候同时对静态字段进行了初始化,并且创建单例对象,以后只管用即可。

类加载的时候,线程还未被创建。不会存在线程安全问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
private static Singleton singleton=new Singleton();

private Singleton() {
//构造函数私有化,不允许在类外创建对象
try {
Thread.sleep(100L);//模拟创建对象耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s-创建对象\n", Thread.currentThread().getId());
}

public static Singleton getInstance() {
return singleton;
}
}

1.3 区别

效率:饿汉模式没有加任何的锁,因此执行效率高。懒汉模式一般会使用锁,效率相比饿汉会差一点,不过也只是在于高并发创建的时候,只要对象创建了,其实也没差别了。

空间:饿汉模式在一开始类加载就实例化,无论是否使用,都会实例化,所以会占据空间,占用内存。而懒汉式是按需加载。

二、致谢

参考

发布:2022-06-28 22:29:42
修改:2022-07-23 01:57:31
链接:https://meethigher.top/blog/2022/singleton/
标签:java 
付款码 打赏 分享
Shift+Ctrl+1 可控制工具栏