单例模式
2021-01-26 / highPhone啊

单例模式是GoF23中设计模式中最常见的设计模式中,它被用以在多线程环境中保持类的实例只有一个。

饿汉式

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package xyz.highphone.singleton;

//final 指定该类不可被继承,通常情况下,构造器私有后singleton类也不能被继承
public final class SingleTon1 {

//实例变量
private byte[] data = new byte[1024*1024];

//定义提供给外部调用的instance,static并在类加载的时候初始化,保证全局唯一
private static SingleTon1 instance = new SingleTon1();

//构造器私有,不允许外部代码new实例
private SingleTon1()
{

}

//提供public static方法给外界获取单例
public static SingleTon1 getInstance()
{
return instance;
}

}

总结

饿汉式单例在多线程环境中能保证实例唯一,是安全的,同时由于在类加载的时候就已经创建实例了,所以在getInstance()的时候只需要返回实例引用即可,效率较高。
但是,由于是类加载的时候进行了new Instance操作,即不是懒加载的,所以单例类的实例变量也会被同时实例化,在堆中分配内存空间,这样可能会造成内存浪费。

懒汉式

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final class Singleton2 {
//实例变量
private byte[] data = new byte[1024*1024];

//定义提供给外部调用的instance,类加载时先不初始化,在需要用到时才初始化
private static Singleton2 instance = null;

//构造器私有,不允许外部代码new实例
private Singleton2()
{

}

public static Singleton2 getInstance()
{
if(null == instance)
instance = new Singleton2();
return instance;
}

}

总结

懒汉式单例是懒加载方式,在第一次调用getInstance()时,才会去实例化单例。但是懒汉式单例在多线程环境中并不安全,当某线程进来判断null == instance时,会去进行实例化,而如果实例化还没完成,又有第二条线程进来,这是instance还是null,第二条线程也会去进行实例化,所以懒汉式单例在多线程环境可能存在多个实例。

懒汉式 + 同步方法

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final class Singleton3 {
//实例变量
private byte[] data = new byte[1024*1024];

//定义提供给外部调用的instance,类加载时先不初始化,在需要用到时才初始化
private static Singleton3 instance = null;

//构造器私有,不允许外部代码new实例
private Singleton3()
{

}

public static synchronized Singleton3 getInstance()
{
if(null == instance) {
instance = new Singleton3();
}
return instance;
}

}

总结

在懒汉式的基础上,通过给getInstance()方法加锁,可以让单例类在多线程中保证实例唯一,并且也实现了懒加载。但这种在整个方法上加锁的方式,让同一时刻只能有一个线程访问到getInstance()方法,这样非常影响性能。

Double-Check

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
public final class Singleton4 {
//实例变量
private byte[] data = new byte[1024*1024];

//定义提供给外部调用的instance,类加载时先不初始化,在需要用到时才初始化
private static Singleton4 instance = null;

//成员变量
private String user;
private Address address;

//构造器私有,不允许外部代码new实例
private Singleton4()
{
this.user = "张三";//实例化user
this.address=new Address();//Address
}

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

总结

Double Check方式,即实现了懒加载,又保证了多线程环境下单例类的实例唯一,另外,在同一时刻只有一个线程能访问同步代码块,但是当单例创建后,后续多线程同时访问getInstance()时,instance都不为null,不需要再走同步代码块了,保证了效率。但是这种实现方式在多线程环境中有可能会引起空指针异常。
分析如下:
在某线程第一次进入getInstance()方法的时候,此时instance是null,此线程将获得锁,进行单例实例化操作,此单例的构造函数中,还实例化了另外两个对象user和address,即instance的实例化中,有如下三个步骤

  1. 实例化instance
  2. 实例化user
  3. 实例化address
    因为这三个对象的实例化,并没有相互依赖的先后顺序,所以,根据JVM指令重排和Happens-Before原则,此时有可能是instance已完成实例化,但user和address还没完成实例化,而此时恰好有第二条线程调用getInstance()获取instance,对第二条线程来说,instance已经不为null了,返回instance使用,则使用instance调用成员变量user和address就会抛出空指针异常。

Volatile + Double-check

代码实现

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
public final class Singleton5 {
//实例变量
private byte[] data = new byte[1024*1024];

//定义提供给外部调用的instance,类加载时先不初始化,在需要用到时才初始化
private volatile static Singleton5 instance = null;

//成员变量
private String user;
private Address address;

//构造器私有,不允许外部代码new实例
private Singleton5()
{
this.user = "张三";//实例化user
this.address=new Address();//Address
}

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

}

总结

由于JVM指令重排序,让Double-Check方式在多线程环境下可能会出空指针异常,所以我们可以为instance静态变量增加Volatile关键字限定,禁止指令重排,这样,Volatile+Double-Check方式实现的单例模式,可以满足多线程下的唯一实例、懒加载及高效的特性。

Holder方式

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Singleton6 {
//实例变量
private byte[] data = new byte[1024*1024];

//构造器私有,不允许外部代码new实例
private Singleton6()
{

}

//定义私有的静态内部类,在内部类中定义Singleton6的实例,并且可以直接初始化
private static class Holder
{
private static Singleton6 instance = new Singleton6();
}

//在getInstance()中,调用静态内部类里面的instance成员变量
public static Singleton6 getInstance()
{
return Holder.instance;
}
}

总结

  • 在Singleton6的成员变量中,并没有instance静态成员变量,而是把instance放到了静态内部类Holder里面,这样,Singleton6类加载时其静态内部类Holder不会被加载,所以不会创建instance实例,当Holder被主动引用的时候才会创建instance实例,这样实现单例的方式满足了懒加载的特性。
  • Singleton6的创建过程会在程序编译时期收集至 ()方法中,而在《深入理解JAVA虚拟机》有这样的描述:虚拟机会保证一个类的()方法在多线程的环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。所以,Holder方式实现的单例模式,是可以满足多线程环境中的唯一实例,且高效率的特性的。Holder方式的单例设计是最好的设计之一,也是目前使用比较广的设计之一。

枚举方式

代码实现

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
public class Singleton7 {
//实例变量
private byte[] data = new byte[1024*1024];

//构造器私有,不允许外部代码new实例
private Singleton7()
{

}
//定义枚举类型的holder
private enum EnumHolder
{
//定义一个枚举实例对象,该对象天生就是单例的
INSTANCE;
private Singleton7 instance;
EnumHolder()
{
this.instance = new Singleton7();
}

private Singleton7 getSingleton7()
{
return instance;
}

}

public static Singleton7 getInstance()
{
return EnumHolder.INSTANCE.getSingleton7();
}
}

总结

枚举类型不允许被继承,同样是线程安全的,而且只能被实例化一次,同时instance定义在单例类内部的枚举类型中,也满足了懒加载特性。并且枚举类型只能被实例化一次,所以保证了单例不会被反射机制破坏掉,所以,从某种意义上来说,通过枚举来实现单例是最好的设计方式了。

本文链接:https://highphone.xyz/f1601c3e.html