目录
1. 引言
在软件开发中,我们经常会遇到需要确保某个类只有一个实例存在的情况,在这种情况下,单例模式成为了一种强大而常用的设计模式。
想象一下,当我们需要访问应用程序的配置信息时,我们希望有一个唯一的配置对象来统一管理这些信息,这时,单例模式就能派上用场。
然而,单纯地使用单例模式并不足以解决所有问题,在实现单例模式时,我们需要考虑到线程安全性、延迟加载等因素,以确保其在不同情况下的正确性和性能。因此,本文将手把手地介绍如何实现单例模式,从简单的懒汉式到更加复杂的线程安全方案,帮助读者深入理解这一设计模式的原理和实现方式。
2. 单例模式
单例模式是一种创建型设计模式,它保证一个类仅有一个实例,并提供一个全局访问点来访问这个实例。单例模式有多种实现方式,其中包括饿汉式、懒汉式、懒汉式加同步锁、双重校验锁、静态内部类和枚举等六种方式。
3. 饿汉式单例模式
class HungrySingleton {
// 在类加载时就创建实例并初始化
private static final HungrySingleton instance = new HungrySingleton();
// 私有化构造方法,防止外部new新的对象
private HungrySingleton() {
}
// 提供一个公有的静态方法来获取实例,当使用到该方法时,直接获取instance,即预加载
public static HungrySingleton getInstance() {
return instance;
}
}
优缺分析
优点:实现简单,不存在线程安全,实例在类加载时就已经创建好,性能较好。
缺点:因为在类加载时就创建了实例,即使在整个应用程序的生命周期中没有被使用,实例也会一直存在,可能会造成资源浪费。
4. 懒汉式单例模式
class LazySingleton{
// 声明一个私有静态变量来保存实例
private static LazySingleton instance;
// 私有化构造方法,防止外部new新的对象
private LazySingleton(){}
// 提供一个公有的静态方法来获取实例,当使用到该方法时,才去创建instance,即懒加载
public static LazySingleton getInstance(){
if(instance==null){
instance=new LazySingleton();
}
return instance;
}
}
优缺分析
优点:懒汉式单例模式在需要时才创建实例,可以延迟对象的创建过程,节省资源并提高性能。
缺点:存在线程安全问题,可能会创建多个实例。
4.1 懒汉式代码实现优化(加锁)
为了解决上文提到的懒汉式单例模式的线程安全性问题,可以使用同步锁来确保在多线程环境下只创建一个实例。
class LazySingleton{
// 声明一个私有静态变量来保存实例
private static LazySingleton instance;
// 私有化构造方法,防止外部new新的对象
private LazySingleton(){}
// 提供一个公有的静态方法来获取实例,当使用到该方法时,才去创建instance,即懒加载
public static synchronized LazySingleton getInstance(){
if(instance==null){
instance=new LazySingleton();
}
return instance;
}
}
或者这样加锁
class LazySingleton {
// 声明一个私有静态变量来保存实例
private static LazySingleton instance;
// 私有化构造方法,防止外部new新的对象
private LazySingleton() {
}
// 提供一个公有的静态方法来获取实例,当使用到该方法时,才去创建instance,即懒加载
public static LazySingleton getInstance() {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
return instance;
}
}
优缺分析
优点:解决了懒汉式单例模式的线程安全性问题。
缺点:加锁会造成性能损耗。
4.2 懒汉式代码实现优化(双重校验锁)
虽然使用了同步锁可以解决线程安全的问题,但是在多线程并发访问的情况下,仍然会有多个线程竞争同一把锁,可能会导致性能下降和线程阻塞,同时还有可能出现指令重排的问题,故使用双重校验锁进行优化。
class LazySingleton {
// 声明一个私有静态变量来保存实例,使用volatile关键字,防止指令重排
private static volatile LazySingleton instance;
// 私有化构造方法,防止外部new新的对象
private LazySingleton() {
}
// 提供一个公有的静态方法来获取实例,当使用到该方法时,才去创建instance,即懒加载
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
// 双重检查,防止多个线程同时通过第一个if语句
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
优缺分析
优点:采用了双重校验锁的方式,大部分情况下不需要进入同步代码块,提高了性能。
缺点:双重校验锁的实现相对比较复杂,包含了两次检查实例是否为null的逻辑,增加了代码的复杂性和理解难度。
5. 静态内部类实现单例模式
静态内部类实现的单例模式是一种比较优雅且安全的方式,它能够保证懒加载、线程安全,并且没有性能损耗。
class Singleton {
// 私有化构造函数,防止外部实例化
private Singleton() {
}
// 静态内部类,在需要时加载,保证了懒加载
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
// 提供一个公有的静态方法来获取实例
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
优缺分析
优点:具有懒加载、线程安全和高效性特点,同时避免了反序列化破坏单例的问题。
缺点:无法传递参数和被继承,并且需要特别注意序列化问题。
6. 枚举实现单例模式
枚举实现单例模式是一种简洁、安全且线程安全的方式,它充分利用了枚举类型的特性,保证了单例的唯一性和防止反序列化破坏单例的问题。
enum Singleton {
// 枚举单例
INSTANCE;
// 可以在枚举类中添加其他方法
public void Method() {
}
}
优缺分析
优点:枚举实现的单例模式在类加载时就创建实例,保证了线程安全性,同时天然地避免了反序列化破坏单例的问题。
缺点:无法延迟加载实例和传递参数给构造函数。
7. 破坏单例模式的问题
7.1 反射破坏单例模式
反射可以破坏传统的单例模式实现,因为它可以通过私有构造函数来创建新的实例。这可能会导致单例模式的唯一性受到破坏,从而违反了单例模式的原则。我们可以在单例模式的私有构造函数中添加逻辑检查,抛出异常,防止反射创建新的实例。
7.2 序列化破坏单例
序列化和反序列化可能会破坏传统的单例模式实现,因为反序列化过程中会通过构造函数创建新的实例,从而导致单例模式的唯一性受到破坏。我们可以在 readObject()
方法中返回已存在的单例实例,确保反序列化过程中不会创建新的实例。