一、前言
最近在做一个项目的时候,发现里面用了大量的单例模式,然后基于自己对于单例的一个理解想写一下应该如何优雅的实现一个单例
二、单例设计模式简介
1.什么是单例模式呢?
单例模式其实简单的来说就是一个类在整个应用程序有且只有一个实例在运行
2.单例模式的特点
- 私用的构造函数,因为要确保在整个应用程序运行的时候只存在一个单例,首先必须要先保证该类不能被实例化,否则会产生多个实例,也就违反了单例的原则
- 自行创建实例,提供一个方法给我们可以调用到这个唯一的实例,既然是私有的构造的函数,那也就意味着不能去实例化,所以要提供一个入口给调用者能够获取到这个唯一的实例
三、单例模式的写法
首先已经初步大概了解了单例的一个特点,下面开始单例模式的代码编写
1.单例模式第一版(饿汉模式)
1
2
3
4
5
6
7
8
9
10
11
12
package com.yate.singleton;
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
public static void print(){
System.out.println("这是一个单例模式");
}
}
上面这段代码很简单,就是定义了一个私有的静态字段,在我们类加载的时候初始化instance,然后提供一个getInstance()方法给外部访问我们的这个实例,下面我们看看第二版的一个单例模式。
2.单例模式第二版(懒汉模式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.yate.singleton;
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
public static void print(){
System.out.println("这是一个单例模式");
}
}
第二版本代码比第一版的稍微复杂了点,但还是非常简单,首先还是定义一个私有的静态字段,只不过在getInstance()方法里面多了一重判断,判断当前的这个instance是否为空,如果是null,就代表在当前应用程序中还未存在这个实例,就先初始化出来,如果有就直接返回当前实例。
1
2
3
4
5
6
7
8
9
10
11
接下来我们对比分析一下这两段代码的一个具体的存在的问题:
第一版的代码,是当我们去加载这个Singleton类的时候,就会先去初始化被static修饰的变量,
也就是我们定义的instance,但其实有时候我们可能根本不需要用到instance这个对象,比如现在
我只需要调用里面的print()方法进行一个打印输出,但是由于我们设置了instance是一个静态变量,
根据类加载顺序,先加载静态变量,那就会造成浪费空间,浪费内存,这就是一个饿汉模式。
第二版的代码,并没有先去初始化这个instance,而是通过一个延迟加载的方式来实现对象的实例化,
当我们调用getInstance()方法的时候,才会去初始化instance对象,这就是懒汉模式,但是这里
存在一个线程安全的问题,就是当多个线程同时去调用getInstance()方法可能会产生多个实例,
破坏了单例模式的原则。
3.单例模式第三版(线程安全的懒汉模式)
1
2
3
4
5
6
7
8
9
10
11
12
package com.yate.singleton;
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
第三版代码在getInstance()方法那里加了一个synchronized同步锁,保证了每次只能有一个线程进入该方法,但是使用这种重量级加锁的方式,当成千上万个线程同时来调用getInstance()方法,就会产生很严重的性能问题,每次只能有一个线程去执行,那就代表其它线程只能阻塞挂起。
4.单例模式第四版(基于第三版性能优化的线程安全的懒汉模式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.yate.singleton;
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
第四版代码就稍微有点小复杂了,首先我们看一下getInstance()方法,我们发现使用了两个相同的判断,先判断instance是否为null,如果为null再进行加锁,加锁完后再进行判断一次是否为null,所以在第一次判断的时候,如果instance不为null,就不用进行下面的初始化加锁操作,相较于第三版的代码得到了一个很大的性能上面的提升,可能以为到这里,这个懒汉模式已经完美了,既实现了线程安全,还降低了性能的开销,但是实际上单看getInstance()方法里面的代码,其实是存在问题的,可能我们会拿到一个尚未完成初始化的对象实例,下面来详细分析一下原因:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
instance = new Singleton() 这一句代码其实在真正执行中,可以分解为一下三行:
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
上面的代码可能会发生一个重排序的情况,简单的来说就是jvm编译器根据自己的一个策略
对我们编写的代码在保证不影响结果的前提下进行优化排序,可能会变成以下这样:
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
ctorInstance(memory); // 2:初始化对象
从上面我们可以看到可能会在jvm进行重排序后,对象的分配内存会早于初始化对象,
可能会导致线程A在执行到instance = new Singleton()的时候,刚好到了分配内存地址,
线程B就进来,来到第一个判断instance是否null那里,这时候的instance是不为null的,
就会去访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化。
5.单例模式第五版(基于第四版使用volatile解决重排序带来的问题)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.yate.singleton;
public class Singleton {
private volatile static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
第五版代码,在对instance变量那里多加一个volatile修饰,volatitle主要包含两个功能,一个是保证内存可见性,一个是禁止重排序,这里用到了它的禁止重排序的功能,保证写操作一定发生在读操作之前,也就是说当线程A去执行instance = new Singleton()的时候,不管这里的jvm如何进行一个优化重排序,线程B在进来getInstance()方法这里,执行第一个instance == null的时候,拿到的会是一个完整已经初始化好的操作,因为instance = new Singleton()是一个赋值也就是写操作,instance == null是一个读操作,写操作会发生在读操作之前,所以可以保证拿到的对象是一个完整的。
6.单例模式第六版(基于第四版使用静态内部类解决重排序带来的问题)
1
2
3
4
5
6
7
8
9
10
11
package com.yate.singleton;
public class Singleton {
private Singleton(){
}
private static class InnerSingleton{
public static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return InnerSingleton.instance;
}
}
第六版代码,是通过一个静态的内部类来解决重排序问题,首先定义了一个InnerSingleton的静态内部类,里面声明了一个静态的instance的变量,当我们调用getInstance()方法的时候,会返回InnerSingleton的instance实例,这样也是可以保证线程安全,并且代码更加简洁,高效,来具体分析一下,为什么这样也是可以实现线程安全,并且还解决了重排序问题。当线程A和线程B同时进来这个getInstance()方法,因为这是一个static方法,所以会进行类加载,也就是loadClass操作,这个时候,会获得一个初始化锁,比如线程A先执行了getInstance()方法,就会获得一个初始化锁,那么线程B就只能阻塞挂起,线程A然后执行InnerSingleton.instance,这时候的instance已经初始化load出来了,当线程A释放初始化锁的时候,线程B进入这个方法,发现这个已经被初始化过了,在java中,静态变量只会在类加载的时候进行初始化赋值,所以线程B就会拿到已经被线程A初始化的instance,然后释放锁,这个实现方式,从整体来看,发现好像是个饿汉模式实现思路,但实际上,只有当我们去调用getInstance()方法的时候,才会触发类加载,从而去获取到想要的这个instance实例,所以本质上还是一个懒汉模式
7.单例模式第七版(基于枚举实现的饿汉模式)
1
2
3
4
package com.yate.singleton;
public enum SingletonEnum {
INTSTANCE;
}
第七版代码,特别简洁、高效、舒服,下面来分析一下为什么使用枚举,短短一行代码能保证线程安全。首先,我们需要从内部去理解一下枚举,通过javap -c SingletonEnum反编译出来的代码,如下:
大概整理一下,大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class SingletonEnum extends java.lang.Enum {
public static final SingletonEnum INSTANCE;
private static SingletonEnum [] VALUES;
static {
INSTANCE = new SingletonEnum("INSTANCE", 0);
VALUES = new SingletonEnum[] {INSTANCE};
}
private SingletonEnum(String name, int original) {
super(name, original);
}
public static SingletonEnum [] values() {
return VALUES.clone();
}
public static SingletonEnum valueOf(String name) {
return Enum.valueOf(SingletonEnum.class, name);
}
}
看到枚举反编译后的代码,估计很容易就知道为什么枚举可以保证线程安全了,跟前面所说的静态内部类异曲同工之妙,当多个线程进来的时候,其中一个线程会获得初始化锁,后面的线程就会阻塞挂起,获得锁的线程进行初始化,然后释放锁,后面进来的线程拿到的instance就都是已经初始化后的,因为在java中static代码快只会调用一次,所以这里保证了线程安全,但是枚举的好处远远不止这些,所有的单例模式都存在一个问题,那就是如果实现了Serializable接口之后,就不再是单例了,为什么呢?这是因为每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。而在java中,每一个枚举以及里面的枚举变量在jvm中都是唯一的,也就是枚举就是天然的单例模式,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。具体调用方式就是SingletonEnum.INSTANCE就可以获得这个实例了。
所以使用枚举来实现单例模式是非常高效,简洁,优雅的,在efftive java第二版中,作者也推荐使用枚举来 实现单例。所以以上就是所有的如何优雅的写出一个单例模式的内容。