一、单例模式的概念
单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点来访问该实例,是 Java 中最简单的设计模式之一。该模式常用于需要全局唯一实例的场景,例如日志记录器、配置管理、线程池、数据库连接池等。
注意:单例模式的构造方法必须要声明为private类型。
欢迎关注工 众号:ItBeeCoder,查看更多高质量技术文章,发送“后端”获取资料
二、实现方式
在Java中,单例模式有多种实现方式,每种方式都有其适用场景和优缺点。下面主要从每种方式的特点、Java代码实现方式及优缺点三个方面介绍几种常见的实现方式。
1.饿汉式(Eager Initialization)
1)特点
类加载时立即创建并初始化实例,是线程安全的,但由于有些类的实例并不会立即用到,这种方式可能会浪费资源,尤其是创建占用资源较多的大对象时,尤为明显。
2)代码实现
public class EagerSingleton {// 类加载时直接创建实例private static final EagerSingleton instance = new EagerSingleton();// 私有构造函数防止外部实例化,这里构造函数必须为私有的private EagerSingleton() {}public static EagerSingleton getInstance() {return instance;}
}
3)优缺点
使用这种方式创建单个示例的优缺点都比较明显。
优点:简单、线程安全。
缺点:实例在类加载时创建,若从未使用会导致资源浪费。
对于计算机技术感兴趣的读者,可以加入下裙,和更多行业爱好者分享、交流最新的技术。
2. 同步方法懒汉式(Lazy)(线程安全,性能差)
1)特点
这种方法通过在实例创建方法getInstance()前加synchronized关键字保证多线程环境下只能创建一个实例对象,这种方法保证线程安全,但性能较低(使用了synchronized 关键字,会导致操作系统用户态和内核态的切换,耗费时间和性能,深层原因后面会在介绍synchronized 关键字的文章里详细介绍)。
2)代码实现
public class SynchronizedSingleton {private static SynchronizedSingleton instance;private SynchronizedSingleton() {}public static synchronized SynchronizedSingleton getInstance() {if (instance == null) {instance = new SynchronizedSingleton();}return instance;}
}
3)优缺点
优点:这种方法是线程安全的,synchronized关键字保证多线程环境下只能创建一个实例对象。
缺点:每次调用getInstance()都需要同步,性能差。
欢迎关注工 众号:ItBeeCoder,查看更多高质量技术文章,发送“后端”获取资料
3. 懒汉式(Lazy Initialization,不推荐,了解即可)
1)特点
该种实现方式延迟初始化实例对象,存在多线程安全问题。
2)代码实现
public class LazySingleton {private static LazySingleton instance;private LazySingleton() {}public static LazySingleton getInstance() {A) if (instance == null) {B) instance = new LazySingleton();}return instance;}
}
3)优缺点
优点:延迟加载,节省资源。
缺点:这里instance实例前未加volatile关键字,并且getInstance()方法未加同步关键字或者加锁,这种方法是非线程安全的,多线程环境下可能创建多个实例。
例如:有两个线程a和b同时要创建LazySingleton 类的实例对象,a、b线程执行到 A) 处时,满足if中的判断条件,就会创建一个LazySingleton 类的实例对象。b线程这时也满足if条件,同样会创建一个实例,所以这种方法并不能保证多线程环境下只创建一个对象。
笔者注:这种方式建议了解即可,真正项目中不要用这种方式。
4. 双重检查锁定(Double-Checked Locking,DCL,推荐该种方法)
1)特点
减少同步次数,兼顾性能与线程安全。
欢迎关注工 众号:ItBeeCoder,查看更多高质量技术文章,发送“后端”获取资料
2)代码实现
public class DCLSingleton {// 这里使用volatile的作用是禁止指令重排序,因为JAVA中创建对象包括几步:如果不使用volatile,private static volatile DCLSingleton instance;private DCLSingleton() {}public static DCLSingleton getInstance() {A) if (instance == null) { // 第一次检查B) synchronized (DCLSingleton.class) {C) if (instance == null) { // 第二次检查D) instance = new DCLSingleton();}}}return instance;}
}
3)优缺点
优点:这种方法是线程安全的,并且在真正用到对象的时候,才会进行对象的创建(懒汉式,延迟创建),避免了提前创建对象导致的资源浪费,性能较好。
缺点:volatile关键在JDK 1.5及以后的版本才支持,JDK1.5以前的版本并不支持该种写法,需要注意,另外代码实现方式稍微复杂一些。
4)该方法使用Volatile关键字的原因
假设当前有两个线程1和2都要获取DCLSingleton 类的实例,线程1执行到A)处,满足条件进入到if判断中,接着进入到同步代码块,执行到B)行代码后,线程1的CPU执行时间片用完,让出CPU,线程2开始执行,线程2满足A)、C)处的判断条件,一直执行到D)处,并开始创建对象。Java中对象的创建主要分为几个步骤:1)声明一个引用变量(值为null);2)创建对象并为对象分配内存空间;3)初始化对象4)将对象的内存地址赋给引用变量。JVM在执行代码指令时,会根据情况对指令进行重排序。如果JVM将创建对象的指令重排序为2)、3)、1)、4),线程2执行完2)和3)后时间片用完,这些对象的引用仍为NULL。线程1)接着执行,会重新创建一个对象。最终导致多线程环境下创建多个实例。
volatile关键字可以防止指令重排序,所以这里在instance实例声明前加了该关键字,创建对象的时候会完全按照1)、2)、3)、4)的步骤执行,避免多线程环境下线程不安全的问题。
5. 静态内部类(Holder模式)
1)特点
利用类加载机制保证线程安全,延迟加载且无需同步。
2)代码实现
public class HolderSingleton {private HolderSingleton() {}// 静态内部类持有实例private static class Holder {private static final HolderSingleton INSTANCE = new HolderSingleton();}public static HolderSingleton getInstance() {return Holder.INSTANCE;}
}
3)优缺点
优点:线程安全、延迟加载、无同步开销。
缺点:无法传递参数初始化实例。
6. 枚举(Enum)
1)特点
枚举是最简洁、安全的单例实现方式,天然防反射和序列化破坏。
2)代码实现
public enum EnumSingleton {INSTANCE; // 单例实例// 添加其它业务方法public void doSomething() {System.out.println("Singleton method");
// 其它业务代码}
}
3)优缺点
优点:线程安全、防止反射攻击、自动处理序列化。
缺点:无法继承其他类(枚举类已继承Enum)。
Java中单例模式有多种实现方式,包括饿汉式、懒汉式、同步方法懒汉式、双重检查锁定、静态内部类和枚举。每种方式都有其优缺点和适用场景:饿汉式简单但可能浪费资源;懒汉式延迟加载但非线程安全;同步方法懒汉式线程安全但性能低;双重检查锁定和静态内部类都提供了线程安全和延迟加载的解决方案;枚举是最简洁安全的单例实现,防止反射和序列化破坏。文章总结了对不同场景的选择建议,并提醒谨慎使用单例模式,以防代码耦合度高和难以测试。
笔者在项目开发过程中一直用的是本文介绍的方法双重检查锁定。这种方法属于延迟加载,能避免提前创建实例导致的资源浪费。
单例模式的核心要点有3方面:
1)唯一性:保证一个类在系统中只有一个实例。
2)控制访问:提供一个全局访问点来获取该实例。
3)延迟初始化(可选):实例化延迟到第一次使用时。
使用建议:
对于需要延迟加载且高并发的场景,建议优先选择静态内部类或双重检查锁定。在需要防反射或序列化攻击的场景,则必须使用枚举。
又到了金三银四求职季,我整理了一些互联网大厂的面试题,有需要的可关注工 众号:ItBeeCoder,发送“后端”获取