一、单例模式简介
1. 定义
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,该设计模式属于创建型模式。这种设计模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。
2. 特点
- 单例类只能有一个实例;
- 单例类必须自己创建自己的唯一实例;
- 单例类必须给所有其他对象提供这一实例。
3. 实现关键
- 构造函数是私有的;
- 一个静态的变量用来保存单实例的引用;
- 一个公有的静态方法用来获取单实例的引用,如果实例为null即创建一个。
二、单例模式实现
单例模式的实现有很多种方式,根据需求场景大致可以分为2大类。其中一类是初始化单例类时就创建单实例,另一类是延迟创建单实例(即使用时才创建,亦即懒加载)。
1. 初始化单例类时创建单实例
(1)饿汉式
该创建方式基于JVM的类加载机制,保证单实例只会被创建一次,从而避免了多线程的同步问题,是线程安全的。其优点是实现方式简单,缺点是一旦类被加载,单实例就会初始化,没有实现懒加载。
- JVM在类的初始化阶段,即在Class被加载后、被线程使用前,会执行类的初始化;
- 在执行类的初始化期间,JVM会去获取一个锁,可以同步多个线程对同一个类的初始化。
public class Singleton {
// 静态变量保存单实例的引用
private static Singleton INSTANCE = new Singleton();
// 构造函数私有
private Singleton() {
}
// 公有的静态方法用来获取单实例的引用
public static Singleton getInstance() {
return INSTANCE;
}
}
(2)枚举
该创建方式利用的枚举的特性实现单例,由JVM保证线程安全,是最简洁、易用的单例实现方式。
《Effective Java》:单元素的枚举类型已经成为实现 Singleton 的最佳方法。
public enum Singleton {
// 定义一个枚举的元素,即为单例类的一个实例
INSTANCE;
}
2. 延迟创建单实例(懒加载)
(1)懒汉式
该创建方式的特点是需要单例时才创建单实例,即实现懒加载。但是,这种创建方式会导致一个问题,那就是线程不安全。当多个线程并发调用getInstance方法时,可能会创建多个实例,从而导致单例模式失效,因此需要对其进行改进和优化。
public class Singleton {
// 类加载时先不创建单例对象,单实例引用置为null
private static Singleton INSTANCE = null;
// 构造函数私有
private Singleton() {
}
// 需要单例时才创建单实例
public static Singleton getInstance() {
// 先判断单实例是否为空,以避免重复创建
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
(2)同步锁(懒汉式改进)
该创建方式是对懒汉式的改进,通过使用同步锁(synchronized)机制创建单实例方法 ,避免多个线程同时调用造成单例被多次创建,从而达到线程安全的目的。但是,这种同步锁的方法又会带来性能问题,即同一时间内只有一个线程能够调用getInstance方法,其它线程则会因为被阻塞而一直等待,加锁导致耗费时间和性能,因此还需对同步锁进行再次优化。
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {
}
public static Singleton getInstance() {
// 加入同步锁
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
(3)双重校验锁(同步锁改进)
该创建方式是对同步锁的改进,即在同步锁的基础上,再添加一层if判断若单例已创建,则不需再执行加锁操作就可获取实例,从而提高性能。执行两次检测很有必要的,当多线程调用时,如果多个线程同时执行完了第一次检查判断,其中一个进入同步代码块创建了实例,后面的线程因第二次检测判断就不会再创建新实例。
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
// 加入同步锁
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
但但但是,这种创建方式看起来似乎很完美,但仍旧存在问题,原因归根结底是INSTANCE = new Singleton()并非是一个原子操作。
事实上,INSTANCE = new Singleton()这句话在JVM中大概做了下面 3 件事情:
1.给 INSTANCE 分配内存;
2.调用 Singleton 的构造函数来初始化成员变量;
3.将 INSTANCE 对象指向分配的内存空间,执行完这步 INSTANCE 就不是null了。
JVM的即时编译器中存在指令重排序的优化,也就是说上面的第2步和第3步的顺序是不能保证的,最终的执行顺序可能是1-2-3也可能是1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 INSTANCE 已经不是null了(但却没有初始化),所以线程二会直接返回 INSTANCE,然后使用,接着报错。
解决方法很简单,我们只需要将 INSTANCE 变量声明成 volatile 就可以了,大概可以改成像下面的样子:
public class Singleton {
// 声明成 volatile
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;
}
}
(4)静态内部类
该创建方式使用静态内部类来创建单实例,实现了懒加载及线程安全。当Singleton被加载时,其内部类并不会被初始化,从而不会创建单实例。只有getInstance() 方法被调用时,静态内部类被加载,此时才会去创建单实例,从而实现了懒加载。同时,基于JVM的类加载机制,保证单实例只会被创建一次,从而避免了多线程的同步问题,是线程安全的。
public class Singleton {
// 构造方法私有
private Singleton() {
}
// 静态内部类
private static class SingletonHolder {
// 在静态内部类中创建单实例
private static Singleton INSTANCE = new Singleton();
}
// 需要单例时才创建单实例
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
网友评论