单例模式

概念

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

饿汉模式

单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

  • 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

  • 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

1
2
3
4
5
6
7
8
9
10
11
// 饿汉式模式单例
public class Singleton {

//类被加载时就创建对象
private static Singleton singleton = new Singleton();
private Singleton(){}

public static Singleton getSingleton(){
return singleton;
}
}

懒汉模式

可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 懒汉式模式单例
public class Singleton {
// 指向自己实例的私有静态引用
private static Singleton singleton;

private Singleton(){}

public static Singleton getSingleton(){
// 被动创建,在真正需要使用时才去创建
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}

上述方式任然是线程不安全的,假设在单例类被实例化之前,有两个线程同时在获取单例对象,线程1在执行完if (instance == null) 后,线程调度机制将 CPU 资源分配给线程2,此时线程2在执行if (instance == null) 时也发现单例类还没有被实例化,这样就会导致单例类被实例化两次。为了防止这种情况发生,需要对 getInstance() 方法同步。:

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static Singleton instance;
private Singleton() {}

// 线程安全的懒汉模式
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

双重检测锁

在加了锁的懒汉模式中,每次获取单例对象时都会加锁,这样就会带来性能损失。双重检测锁实现本质也是一种懒汉模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
}

就算在单例类被实例化时有多个线程同时通过了 if (instance == null) 的判断,但同一时间只有一个线程获得锁后进入临界区。通过if (instance == null) 的每个线程会依次获得锁进入临界区,所以进入临界区后还要再判断一次单例类是否已被其它线程实例化,以避免多次实例化。

由于双重加锁实现仅在实例化单例类时需要加锁,所以相较于上一种实现方式会带来性能上的提升

双重加锁要对 instance 域加上 volatile 修饰符,使用volatile可以禁止指令重排,避免多线程的时候在虚拟机中运行的时候指令重排造成的空指针错误。

由于 synchronized 并不是对 instance 实例进行加锁(因为现在还并没有实例),所以线程在执行到 instance = new Singleton()后,应该将修改后的 instance 立即写入主存(main memory),而不是暂时存在寄存器或者高速缓冲区(caches)中,以保证新的值对其它线程可见。