Java单例模式

单例模式一个是设计模式中最常见的一种了,网上有n多中写法。顾名思义单例模式就是只有一个该类的实例。实现方式大概有一下几类:

  1. 饿汉式
  2. 懒汉式
  3. 枚举模式

首先最基本的肯定是将构造函数设为私有,在此基础上几种写法各有特点,主要是因为三个性能指标的影响:

  • lazy loading
  • 线程安全
  • 序列化攻击

1、懒汉式[不可用]

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {

private Singleton(){}

private static Singleton singleton;

public static Singleton getInstance(){
if(singleton==null){
singleton=new Singleton();
}
return singleton;
}
}
  • 优点:实现了Lazy Loading,第一次使用的时候才真正实例化
  • 缺点:无法保证线程安全

2、饿汉式[可用]

1
2
3
4
5
6
7
8
9
10
public class Singleton {

private Singleton(){}

private final static Singleton INSTANCE=new Singleton();

public static Singleton getInstance(){
return INSTANCE;
}
}
  • 优点:这种写法的优点就是线程安全,利用类加载机制避免了多线程问题
  • 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

3、懒汉式改进-双重检查(还是不安全)

针对懒汉式的线程不安全,可以通过双重加锁检查(DCL)的机制来避免:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {

private Singleton(){}

private static volatile Singleton singleton;

public static Singleton getInstance(){
if(singleton==null){//1
synchronized (Singleton.class){//2
if(singleton==null){//3
singleton=new Singleton();//4
}
}
}
return singleton;
}
}

将singleton声明volatile保证可见性,再通过加锁实现线程安全,最大限度的减少了加锁带来的性能损耗。

更新一下:

在小米面试的时候面试官指出:即使是双重检测,也还是无法保证线程安全。

后来上网找了一下,确实发现了这个以前没有注意到的细节。比如下面这种情况:

  1. 步骤1中线程1检测到singleton为null,于是进入步骤2获取锁,到步骤4中,开始实例化一个实例。
  2. 这里问题出来了,在实例化的过程中,执行构造方法之前,singleton就已经是非null的了
  3. 这个时候如果线程2执行到步骤1,检测到singleton为非null,那么就直接返回这个对象了,这个对象有可能还并没有执行构造函数,也就是说这里检测singleton是否为null,只能保证是否有这个实例了,但是却没法保证这个实例的构造过程是否已经完全执行完毕。

这里涉及到JVM的一个无序写入的问题,总之这个问题以前一直被忽略,直到面试官提出。所以自己闭门造车还是有局限性的,多和牛人交流,也是提升自己的一个办法。

再次更新:

在Java并发编程实战的最后一章倒数第三页上也介绍了这个问题,奈何当时面试的时候还没看到这个部分。

首先否定了这个方法的正确性:线程可能看到一个仅被部分构造的实例。

下面再引用一段书中的总结:

在JVM(书上写的是JMM,我怀疑是写错了)的后续版本中,如果将单例声明为volatile类型,你们就能启动DCL,并且这种方式对性能的影响很小。因为volatile变量读取操作的性能通常只是略高于非volatile变量。

然而,DCL这种使用方法已经被广泛地废弃了,因为促使该模式出现的原因(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在,因而它不再是一种高效的优化措施。延迟初始化占位类模式能带来同样的优势,并且更容易理解。

书中说的延迟初始化占位类模式,也就是下面要介绍的静态内部类。

4、饿汉式改进-静态内部类

饿汉式主要缺点就是无法实现Lazy Loading,可以通过静态内部类来解决这一问题

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {

private Singleton(){}

private static class SingletonInstance {
private static final Singleton INSTANCE=new Singleton();
}

public static Singleton getInstance(){
return SingletonInstance.INSTANCE;
}
}

与上面的直接定义final实例不同的是定义一个静态的内部类,在内部类中持有这个实例,这样只有在第一次获取实例的时候才会加载这个内部类,从而实现Lazy Loading。

5、枚举实现

方法3和方法4都实现了Lazy Loading也保证了线程安全,但单例攻击还会面临一个问题就是序列化。于是就有了枚举类型的单例:

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
public class EnumSingleton {

private EnumSingleton(){}

public static EnumSingleton getInstance(){
return Singleton.INSTANCE.getInstance();
}

private static enum Singleton{
INSTANCE;

private EnumSingleton singleton;

private Singleton(){
singleton=new EnumSingleton();
}

public EnumSingleton getInstance(){
return singleton;
}
}

public static void main(String[] args) {
EnumSingleton a=EnumSingleton.getInstance();
EnumSingleton b=EnumSingleton.getInstance();
System.out.println(a==b);//true
}
}