单例模式Singleton

【摘要】单例模式Singleton就是某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。

前言

当在搜索引擎输入”单例模式”这四个字,我们一定可以看到五花八门关于单例模式的分类,最常见的莫过于饿汉式和懒汉式,还有什么5种的,6种,7种的。要是觉得这很重要的话,不妨考虑一下”茴香豆”的”茴”下面的”回”到底有几种写法。再说个题外话,记得高中时期班上有位同学天天吹嘘自己可以用N英文表示中文中”牛”字的表达方式。哈哈哈,有点扯远了。学习设计模式的过程中不断的深入理解面向对象的思想,灵活地运用到解决实际问题的场景下,这才是我们所需要的。

单例模式(Singleton Pattern)

单例模式属于创建型模式,它提供了一种创建对象的最佳方式。

1.单例类只能有一个实例。
2.必须保证自己创建自己的唯一实例。
3.并且给其他对象提供访问这一实例的方式。

应用场景

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

分类

单例模式常见的有饿汉式、懒汉式、双检锁/双重校验锁、静态内部类、枚举。

饿汉式

这种方式类加载到内存后,就实例化一个单例,JVM会保证线程安全。简单实用,相当推荐使用。但是,他的唯一缺点就是:不管是否用到,类加载时就会完成实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 饿汉式
*/
public class Singleton01 {
private static final Singleton01 INSTANCE = new Singleton01();

private Singleton01() {
}

public static Singleton01 getInstance() {
return INSTANCE;
}

public void m() {
System.out.println("m");
}

public static void main(String[] args) {
Singleton01 s1 = Singleton01.getInstance();
Singleton01 s2 = Singleton01.getInstance();
System.out.println(s1 == s2);
}
}

对于饿汉式的单例模式,还有一种是在静态代码块中进行new一个单例。和上面的本质没有区别,看看就好。

1
2
3
4
5
6
7
8
public class Singleton02 {
private static final Singleton02 INSTANCE;

static {
INSTANCE = new Singleton02();
}
//...
}

懒汉式(线程不安全)

相比饿汉式,懒汉式的单例模式达到了按需初始化的目的,但是带来了线程不安全的问题。

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
/**
* lazy loading 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
*/
public class Singleton03 {
private static Singleton03 INSTANCE;

private Singleton03() {
}

public static Singleton03 getInstance() {
if (INSTANCE == null) {
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
INSTANCE = new Singleton03();
}
return INSTANCE;
}

public void m() {
System.out.println("m");
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Singleton03.getInstance().hashCode());
}).start();
}
}
}

上面这种方式线程不安全,在判断INSTANCE == null的后面我们把线程阻塞1ms,运行后通过比较hashCode就会发现不同线程访问到的实例并不全是同一个对象实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
422057313
503674368
1650594751
1634218452
1160999711
1341620907
1522804050
72786614
1430746659
966186071
1133046463
151286434
2115147268
1326204679
422057313
796465865
796465865
733365800
796465865
796465865
...

懒汉式(线程安全)

为了保证懒汉式单例模式线程安全,考虑到对getInstance()方法的改造。通过synchronized来实现线程安全。

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
/**
* 可以通过synchronized解决,但却带来了效率下降
*/
public class Singleton04 {
private static Singleton04 INSTANCE;

private Singleton04() {

}

public static synchronized Singleton04 getInstance() {
if (INSTANCE == null) {
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
INSTANCE = new Singleton04();
}
return INSTANCE;
}

public void m() {
System.out.println("m");
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Singleton04.getInstance().hashCode());
}).start();
}
}
}

运行测试之后,发现这种方式可以保证线程安全。但是在内存当中的对象一定比我们上面这个简单的Singleton04对象大的多,还有就是在每一次使用的时候都需要去获取这把锁才可以获取实例。这样一来,效率就会降低。
所以就有人想通过减少同步代码块的方式来提高效率,所以就产生了下面这种。

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
/**
* 通过synchronized解决,但却带来了效率下降。所以就想通过减少同步代码块的方式来提高效率,但是不可行。
*/
public class Singleton05 {
private static Singleton05 INSTANCE;

private Singleton05() {

}

public static Singleton05 getInstance() {
if (INSTANCE == null) {
//试图通过减少同步代码块的方式来提高效率,不可行
synchronized (Singleton05.class) {
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
INSTANCE = new Singleton05();
}
}
return INSTANCE;
}

public void m() {
System.out.println("m");
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Singleton05.getInstance().hashCode());
}).start();
}
}
}

上面这种并不能保证线程安全。所以就出现了双检锁。

双检锁/双重校验锁

双检锁/双重校验锁(DCL,即 double-checked locking)这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

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
/**
* 双检锁/双重校验锁
*/
public class Singleton06 {
private static volatile Singleton06 INSTANCE;//JIT

private Singleton06() {

}

public static Singleton06 getInstance() {
if (INSTANCE == null) {
//双重检查
synchronized (Singleton06.class) {
if (INSTANCE == null) {
// try{
// Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
INSTANCE = new Singleton06();
}
}
}
return INSTANCE;
}

public void m() {
System.out.println("m");
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Singleton06.getInstance().hashCode());
}).start();
}
}
}

静态内部类方式

只有第一次调用getInstance()方法时,虚拟机才加载Singleton07Holder内部类 并初始化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
/**
* 静态内部类方式
* JVM保证单例,加载外部类时不会加载内部类,这样可以实现懒加载
*/
public class Singleton07 {
private Singleton07() {

}

private static class Singleton07Holder {
private final static Singleton07 INSTANCE = new Singleton07();
}

public static Singleton07 getInstance() {
return Singleton07Holder.INSTANCE;
}

public void m() {
System.out.println("m");
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Singleton07.getInstance().hashCode());
}).start();
}
}
}

枚举单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 不仅可以解决线程同步,还可以防止反序列化。
*/
public enum Singleton08 {
INSTANCE;

public void m() {
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Singleton08.INSTANCE.hashCode());
}).start();
}
}
}

结束语

文末就不聊单例了。
“茴香豆”的”茴”下面的”回”的几种写法:回、囘、囬、廻;
“牛”字的英文表达方式:ox;(菜牛)beef cattle;(纯种公牛)pedigree bull;(公牛)bull;(母牛)cow;(奶牛)milk cow,dairy cattle;(水牛)water buffalo;(小牛)calf.

评论