0%

设计模式笔记(1)——单例模式

单例模式顾名思义就是保证某个类只有一个实例的设计方式,他负责自己创建实例,且保证实例的唯一性。这种实例的唯一性,可以避免某些高频率使用的对象被频繁的创建和销毁,提高了效率,节省了系统资源。单例模式向外界提供了创建该类实例的方法,并私有化构造方法,使得外界不能直接通过new构造实例。

单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级
对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等)。

单例模式分为饿汉式和饱汉式,下面我们来详细介绍一下这两种设计模式。

二. 饿汉式

饿汉式是通过直接声明一个该类静态成员变量并在加载的时候就直接创建实例。我们可以用以下的方法进行构造,代码如下:

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
package com.mjj.singleton;

public class Singleton01 {

public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton instance1 = Singleton.getInstance();
boolean equals = instance.equals(instance1);
System.out.println(instance == instance1);
System.out.println(instance.hashCode());
System.out.println(instance1.hashCode());


}

}

//饿汉式
class Singleton{

private Singleton(){

}

private static final Singleton instance = new Singleton();

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

饿汉式直接在内部声明对象实例,并提供了一个返回对象实例的方法,这样类在加载的时候就会自动创建一个该类的实例。这种方法实现起来简单易懂,并且自动支持线程安全(由jvm保证)。但是饿汉式的方法又一个缺点就是类在加载的时候就会被创建该实例,不管有没有被使用到,这就导致如果这个实例没有被用到的化,就会造成资源浪费。

三. 懒汉式

如果说饿汉式是一种空间换时间的方法,那么懒汉式就是就是一种时间换空间的方法。懒汉式提供一种懒加载的方式,只有在需要的时候才会创建该类的实例对象。他有很多种方法,下面我们来具体看一下。

1. 线程不安全

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
package com.mjj.singleton;


//懒汉式 只有在获取getInstance的时候才会去加载,并且只会加载一次
// 缺点 线程不安全
public class SingletonTest03 {
public static void main(String[] args) {
Singleton03 instance = Singleton03.getInstance();
Singleton03 instance1 = Singleton03.getInstance();
System.out.println(instance == instance1);
System.out.println(instance.hashCode());
System.out.println(instance1.hashCode());
}
}

class Singleton03{
private static Singleton03 singleton;

private Singleton03(){

}

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

}
}

这种懒汉式将实例化对象封装在方法中,这样可以实现懒加载,也就是只有在用到getInstance()的时候才会加载实例,但是这种方法不是线程安全的,比如有两个线程A和B同时调用getInstance()方法,当A已经进入if(singleton == null)语句之后,还没有跳出if判断语句,B线程也进入到if判断语句,这就会导致创建多个兑现,违背单例模式对象唯一性。为了解决这一问题,我们有3种方法进行改进。

2. 双重检查

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
package com.mjj.singleton;



public class SingletonTest05 {
public static void main(String[] args) {
Singleton05 instance = Singleton05.getInstance();
Singleton05 instance1 = Singleton05.getInstance();
System.out.println(instance == instance1);
System.out.println(instance.hashCode());
System.out.println(instance1.hashCode());
}
}

class Singleton05{
private static volatile Singleton05 singleton;

private Singleton05(){

}

// 双重检测的方法 不仅线程安全 并且能够解决效率底的问题 建议使用
public static Singleton05 getInstance(){
if(singleton == null){
synchronized (Singleton05.class) {
if (singleton == null)
singleton = new Singleton05();
}
}
return singleton;
}
}

这种方法通过两次判断类的实例对象是否被创建,其中第一次判断语句是为了防止线程每一次调用getIntance()都会使用synchronized进行同步,使代码执行效率变高,第二次判断语句是为了防止多线程情况下重复创建对象。这里使用volatile有两个目的,第一是使多线程之间共享资源可见,当一个线程在修改公共资源的时候其他线程能够立刻从内存中读取修改值,第二个好处就是使得指令有序,防止空指针引用。举个例子,当线程A运行到singleton = new Singleton05()时,其实会有3个指令操作

    1. 获取对象地址;
    1. 在对象地址上初始化一个Singleton05对象;
    1. 将singleton引用指向对象地址;
      但是JVM会自动进行指令的优化,他可能会按照1->2->3的方式进行也可能会按照1->3->2的方式进行,当线程A按照第二种方式操作的时候,且已经将singlton引用指向了对象地址了,这时正好线程B运行到第一个判断语句就会判断成非空,然后返回singleton引用,此时引用并没有初始化对象,就是一个空指针,这样就会造成空指针异常。

3. 静态内部类

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
package com.mjj.singleton;



public class SingletonTest06 {
public static void main(String[] args) {
Singleton06 instance = Singleton06.getInstance();
Singleton06 instance1 = Singleton06.getInstance();
System.out.println(instance == instance1);
System.out.println(instance.hashCode());
System.out.println(instance1.hashCode());

}
}

class Singleton06{

private Singleton06(){

}

private static class SingletonInstance{
private static Singleton06 singleton = new Singleton06();
}
// 使用静态内部类 不仅线程安全 并且能够解决效率底的问题 建议使用
public static Singleton06 getInstance(){

return SingletonInstance.singleton;
}
}


静态内部类的方法是创建一个内部类,并在内部类里面创建静态实例对象,内部类必须是私有的只能由外部类调用,这种方法相当于将多线程安全的问题交给JVM去处理,也是推荐使用的方法。

4. 枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.mjj.singleton;

public class SingletonTest07 {
public static void main(String[] args) {
Sinleton07 instance1 = Sinleton07.Instance;
Sinleton07 instance2 = Sinleton07.Instance;
System.out.println(instance1 == instance2);

}
}

enum Sinleton07{
Instance;
public void methof(){
System.out.println("枚举方法 单例模式");
}
}


使用枚举也是一种比较好的方法,这种方法使用简单,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,也是推荐的方法。

参考文献

视频:
尚硅谷Java设计模式,韩顺平图解java设计模式

文献:
双重检查单例为什么要加volatile
单例模式懒汉式和饿汉式区别