需求作用
我们一般代码里编写的时候会使用上单例,主要就是因为有些类的对象是无状态的,在程序运行期间字段的类型几乎不会发生变更,也只需要存在一份实例引用即可。同时如果出现了两个实例,就很可能会导致程序作出异常的行为。
最经典的应用就是应用程序的配置文件映射类,配置类
基于JVM类的饿汉式加载
代码实现
1 2 3 4 5 6 7 8 9 10 11
| public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() { }
public static HungrySingleton getInstance() { return instance; } }
|
分析
这种实现方式首先是一种饿汉式加载,也就是JVM在加载类的时候就完成类对象的创建,并不是用到的时候再加载
同时也是借用了 JVM 加载类的机制来做到线程安全,并不存在线程不安全的问题:因为 JVM 加载静态类只会加载一次,并且类的静态字段初始化也是原子性的操作,因此这种实现方式天然就是线程安全的
懒汉式实现
线程不安全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static LazySingleton getInstance(){ if (instance == null) { instance = new LazySingleton(); } return instance; } }
|
上述实现为线程不安全的实现方式
也就是在多线程场景下,不同线程调用 LazySingleton.getInstance()
可能会创建多个实例
同步方法
为了解决线程不安全的问题,我们可以在方法上加锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class SynchronizedLazySingleton {
private static SynchronizedLazySingleton instance = null;
private SynchronizedLazySingleton() {
}
public static synchronized SynchronizedLazySingleton getInstance() { if (instance == null) { instance = new SynchronizedLazySingleton(); } return instance; }
}
|
上述的实现方式同样也引出了一个问题:由于是在get方法级别进行同步,这就会导致所有调用 getInstance 方法的线程都会阻塞住,存在性能问题
我们可以尝试进行优化:只有当第一次初始化单例对象的时候,才进行加锁防止多线程并发问题,这就引出了我们的双重检查锁的方式
Double-check Locing
双重检查锁的实现,只有在第一次初始化单例对象的时候才加锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class DclLazySingleton {
private static volatile DclLazySingleton instance = null;
private DclLazySingleton() { }
public static DclLazySingleton getInstance() { if (instance == null) { synchronized (DclLazySingleton.class) { if (instance == null) { instance = new DclLazySingleton(); } } } return instance; }
}
|
上面有两次检查(instance == null
),分别对应解决不同的问题:
- 第一次检查,是为了减少不必要的加锁;因为如果没有第一次加锁,那就是上面在方法级别上进行加锁;此时多个线程get,如果实例已经创建了,还是需要阻塞等待,严重影响性能;因此使用第一次检查,只有实例没有初始化的时候才加锁限制创建
- 第二次检查,是为了防止多个等待锁的线程重复创建实例
静态内部类
基于静态内部类实现
这种方法利用的机制是:JVM 在加载外部类的时候,对于静态内部类不会进行加载,只有当内部类的属性或者方法被调用的时候才会加载并进行唯一一次初始化
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() { }
public static StaticInnerClassSingleton getInstance() { return SingletonHolder.instance; }
private static class SingletonHolder { private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton(); }
}
|
最佳实践
推荐使用静态内部类的方式
基于JVM初始化类时对静态变量的原子初始化操作,无锁,线程安全,用到的时候才加载,并发性高