单例模式

本文最后更新于:3 年前

wallpapersden.com_small-memory_7680x4320.jpg

这两天看到了单例模式的面试题,打算把单例模式的知识点整理一下。

概念

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式。

保证一个类仅有一个实例,并提供一个访问它的全局访问点

要注意的地方:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

啥子优缺点

优点

  • 1、在内存里只有一个实例减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 2、避免对资源的多重占用(比如写文件操作)。

缺点

没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

用途

  • 1、要求生产唯一序列号。
  • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

JDK中哪些实现了单例模式

java.lang.Runtime

由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个。所以应该使用单例来实现。

线程池

java.lang.reflect.Proxy类

GUI中

java.awt.Toolkit#getDefaultToolkit()

java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment()

实现方式

方式 Lazy 初始化 多线程安全 实现难度
懒汉式 否/是
饿汉式
DLC 较复杂
内部静态类 一般
枚举

懒汉式

分为多线程安全多线程不安全两种。

多线程不安全

最基本的实现方式,但是由于没有添加synchronized锁,所以不支持多线程,在多线程不能正常工作。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author Sept Zhang
* @create 2021-04-20 14:25
* 懒汉式单例
* 多线程不安全
*/

public class LazyMan {

private LazyMan(){}

private static LazyMan lazyMan;

//在方法是使用时null时创建对象
public static LazyMan getInstance(){
//单线程OK,多线程不安全
if (lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}

多线程测试

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
/**
* @author Sept Zhang
* @create 2021-04-20 14:25
* 懒汉式单例
* 多线程不安全
*/

public class LazyMan {

private LazyMan(){
//多线程测试
System.out.println(Thread.currentThread().getName() + "OK");
}

private static LazyMan lazyMan;

//在方法是使用时null时创建对象
public static LazyMan getInstance(){
//单线程OK,多线程不安全
if (lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
//多线程测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
lazyMan.getInstance();
}).start();
}
}
}

测试结果

1
2
3
4
5
6
7
8
9
10
Thread-2OK
Thread-9OK
Thread-0OK
Thread-6OK
Thread-8OK
Thread-4OK
Thread-3OK
Thread-1OK
Thread-5OK
Thread-7OK

出现多个线程,且每次运行结果不同。

多线程安全

效率很低,99% 情况下不需要同步。

优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {  
private static Singleton instance;
private Singleton (){}
//添加 synchronized 锁
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

饿汉式

比较常用,但容易产生垃圾对象。

优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。

1
2
3
4
5
6
7
public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

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
28
29
30
31
32
33
34
35
36
37
38
package single;

/**
* @author Sept Zhang
* @create 2021-04-20 14:25
* 懒汉式单例
* 多线程安全
*/

public class DCL {

private DCL(){}

private static DCL lazyMan;

//双重检测锁模式的 懒汉式单例, DCL懒汉式
public volatile static DCL getInstance(){
//加锁,使得多线程安全
if (lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new DCL();//不是原子性操作,可能出现指令重排现象。
/**
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把对象指向这个空间
*可能出现指令重排现象。
* 添加 volatile,可以解决指令重排
* volatile:只保证可见性,不保证原子性,但是可以防止指令重排
*/
}
};

}
return lazyMan;
}
}

测试代码

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
package single;

/**
* @author Sept Zhang
* @create 2021-04-20 14:25
* 懒汉式单例
* 多线程安全
*/

public class DCL {

private DCL(){
//多线程测试
System.out.println(Thread.currentThread().getName() + "OK");
}

private static DCL lazyMan;

//双重检测锁模式的 懒汉式单例, DCL懒汉式
public volatile static DCL getInstance(){
//加锁,使得多线程安全
if (lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new DCL();//不是原子性操作,可能出现指令重排现象。
/**
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把对象指向这个空间
*可能出现指令重排现象。
* 添加 volatile,可以解决指令重排
* volatile:只保证可见性,不保证原子性,但是可以防止指令重排
*/
}
};

}
return lazyMan;
}
//多线程测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
lazyMan.getInstance();
}).start();
}
}
}


测试结果

1
Thread-8OK

运行结果只有一个线程。

注意

不加volatile时,由于不是原子性操作,可能出现指令重排现象使得线程不安全。

volatile:只保证可见性,不保证原子性,但是可以防止指令重排

静态内部类

这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

1
2
3
4
5
6
7
8
9
public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

枚举

是实现单例模式的最佳方法。

更简洁,自动支持序列化机制,绝对防止多次实例化。

1
2
3
4
5
public enum Singleton {  
INSTANCE;
public void whateverMethod() {
}
}

参考

菜鸟教程

Hollis


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!