单例模式

单例模式,属于创建类型的一种常见的软件设计模式。通过单例模式可以确保一个类在当前进程中只有一个实例。当然,根据需要,也可能时一个线程中的单例,比如线程上下文内使用同一个实例。

单例模式一共有两种方式:饿汉式,懒汉式

饿汉式

所谓饿汉式就是在类加载的时候就进行初始化目标对象,以后去获取该的单例对象时就直接获取即可。

1
2
3
4
5
6
7
8
9
class Singleton implements Serializable {
private static final Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
public Object readResovle() {
return instance;
}
}
  • 需要设置为private,防止被别的类改动,破坏单例,但是private并不能防止通过反射破坏单例。
  • 需要添加 final ,防止子类中可能会使用什么方法去给 instance赋值,来破坏单例
  • 如果 Singleton 实现了序列化接口, 需要实现readResovle()方法,返回单例对象,来防止反序列化破坏单例。反序列化的时候,如果发现readResovle()返回了一个对象,则会直接将该对象当成反序列化的结果。

饿汉式的好处就是实现起来简单,不需要考虑并发问题。但是由于在类加载的时候就创建了对象,如果在程序运行的过程种没有任何线程去获取该对象(该对象没有被使用),就会浪费内存。

懒汉式

懒汉式主要使用的就是一种懒加载的思想,在程序需要去获取该对象的时候再去创建。

实现一(无法保证单例)不可用

1
2
3
4
5
6
7
8
9
class Singleton {
private static final Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

这种实现过于简单,会出现问题,如果多个线程同时去获取Singleton,如果一个线程执行到if (instance == null) 还没来得及往下执行,另一个线程也进行了这个判断,发现 instance 为空,也去创建了一个实例。这样就会出现多个实例。

实现二(加锁)

解决实现一,最简单的思路就是加锁,让在同一个时间内,只能有一个线程执行获取对象的方法

1
2
3
4
5
6
7
8
9
10
11
class Singleton implements Serializable {
private static Singleton instance;
public static synchronized Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}

只需要加上synchronized,可以保证线程安全。这种方法虽然保证只有一个实例,但是在第一次创建对象以后,其实加锁是没有必要的。但是每次获取对象时都要获取锁,降低了性能低下

实现三(double-check locking)

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton implements Serializable {
private static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

如果检测到instance为null才去竞争。如果已经初始化,就不需要去竞争锁了,直接返回对象即可。但是这种方法并非完美。因为指令重排,会导致有序性出现问题。比如instance = new Singleton()的指令如下:

1
2
3
4
0: new           #3        //1. 加载类(如果需要); 2.堆空间开辟一块内存)       
3: dup //操作数栈里面的引用复制一份(这个不需要关心)
4: invokespecial #4 //调用构造器(这一步完成之后对象那块堆内存才是完整的)
7: putstatic #2 // 给静态变量instance赋值

java虚拟机可能会对其进行优化,进行指令重排。指令 4 和 7 可能会发生重排序,即:先执行 7 然后执行 4,先给静态变量赋值,然后调用构造器构造该对象。

  • 假设有两个线程,线程一和线程二

  • 一开时instance为空,线程一调用getInstance方法,判断instance为空,进入到synchronized代码块

  • 线程一执行new,dup,putstatic。此时还没有执行invokespecial。就已经给静态变量instance赋值了

  • 这个时候线程二也调用了getInstance,判断instance不为空,直接返回了该对象,但是此时instance其实还未被初始化。

解决方法:只需要给instance加一个volatile修饰即可,会再读取instance之前加读屏障(防止读屏障之后的指令重排序到读屏障前),在给instance赋值以后加入写屏障(防止写屏障之前的指令重排序到写屏障后)。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton implements Serializable {
private volatile static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}