什么是单例模式?
简单通俗来讲,单例模式是一种设计模式,它确保一个类只能有一个实例,并提供全局访问。在 Java
中,可以通过以下几种方式来实现线程安全的单例模式。
饿汉式单例
饿汉式单例模式指的是在类加载的时候就创建单例对象。这种方式的实现比较简单,只需要在类的定义中添加一个静态成员变量,该成员变量用于保存单例对象,同时将类的构造方法私有化,以防止外部创建对象。
java
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
注意:
- 饿汉式单例与懒汉式单例的区别在于对象的创建时间。饿汉式单例在类加载时就已经创建了单例对象,并且它是由static final 修饰的,保证了单例对象在整个程序中都只有一个实例,所以不会有性能问题,并且也不需要额外的同步操作来保证线程安全。所以饿汉式单例天生就是线程安全的。但是饿汉式单例在类加载时就创建了单例对象,因此在某些场景下可能会造成内存浪费。饿汉单例模式适用于单例对象创建开销不大,并且程序启动时需要使用单例对象的场景。
例如,在程序启动时就需要加载的配置信息,可以使用饿汉式单例来读取配置文件。因为在程序启动时就需要加载配置文件,而且配置文件只需要读取一次,所以可以使用饿汉式单例。
一个经典的应用场景就是在 Java 中的日志管理类,下面是一个简单的实现:
java
public class Logger {
private static final Logger INSTANCE = new Logger();
private Logger(){
//初始化
}
public static Logger getInstance(){
return INSTANCE;
}
public void log(String message){
//执行日志记录
}
}
这个Logger 类负责管理程序中所有的日志信息,因为在类加载时已经创建了 Logger 对象,所以在整个程序中只有一个 Logger 对象,并且是线程安全的。这样使用时只需要调用Logger.getInstance().log("log message")来记录日志。
还有一个特殊的场景就是在资源限制的情况下,比如设备硬件限制,或者是其他限制条件下,这种情况下因为不能等待长时间创建对象,又因为每个线程都可能会请求单例对象,所以此时就可以使用饿汉单例模式来保证程序在限制条件下的正常运行。
饿汉式单例模式虽然具有线程安全的优点,但在内存占用、类加载速度、扩展性和资源利用效率方面存在一些缺点。
-
类加载时初始化,占用空间:饿汉式单例模式在类加载时就完成了初始化,并创建了单例对象。这意味着无论是否需要使用该单例对象,都会占用一定的内存空间。如果单例对象的创建比较消耗系统资源,而外部一直没有调用该实例,那么这部分的系统资源消耗是没有意义的。
-
类加载较慢:由于饿汉式单例模式在类加载时就完成了初始化,这可能会导致类加载的速度相对较慢,尤其是在存在大量类需要加载的情况下。
-
扩展困难:饿汉式单例模式一般没有接口,扩展起来比较困难。如果要扩展单例对象的功能,只有修改代码这一途径,这不符合程序的开闭原则。
-
不支持延迟加载:饿汉式单例模式不支持延迟加载,即系统启动时就创建了单例对象,无论是否立即使用。这在某些情况下可能不是最优的资源利用方式。
懒汉式单例模式
懒汉式单例模式指的是在第一次使用时才创建单例对象。这种方式的实现相对复杂,需要注意线程安全问题,否则会导致多个线程同时创建多个对象。常见的解决方案是使用双重检查锁定。
java
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上面的示例代码中,通过在 getInstance
方法中使用双重检查锁定来确保只有一个实例被创建。volatile
关键字可以避免指令重排序,以下是具体的说明。
instance = new Singleton()
不是原子操作,这段代码可以简单分为下面三步执行:
-
为
instance
分配内存空间; -
初始化
instance
; -
将
instance
指向分配的内存地址
由于但是由于 JVM
具有指令重排的特性,执行顺序有可能变成 1->3->2
,造成未初始化完全的对象发布。
静态内部类单例模式
这种方式采用了类装载的机制来保证初始化实例时只有一个线程。类的静态属性只会在第一次加载类的时候初始化,在这里,JVM
帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
java
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton () {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
将 instance
放在了内部类 SingletonHolder
中,前面我们提到饿汉式是类加载时就会立即创建对象,而静态内部类不会,它只会在调用了 getInstance
时,才会加载内部类 SingletonHolder
,此时才会创建对象。利用静态内部类特点实现延迟加载,效率高。
枚举
这种方式是 Effective Java
作者 Josh Bloch
提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
java
enum Singleton{
INSTANCE;
public void say(){
System.out.println("hello");
}
}
在上面的示例中,Singleton
被定义为一个枚举,其中只有一个元素INSTANCE
,该元素代表了Singleton
的唯一实例。可以通过Singleton.INSTANCE
来访问这个唯一的实例。
单例模式的使用场景
- 需要确保系统中某个类只有一个实例存在的情况。比如,一个配置管理类,一个日志记录器等。
- 需要对某个类的实例进行严格控制,以确保在系统中始终只有一个实例存在。例如,线程池、数据库连接池等。
- 需要避免创建大量的实例,以节约系统资源的情况。例如,一个类的实例需要占用大量的系统资源,如果创建多个实例可能会导致系统崩溃或者性能下降。
单例模式虽然能够保证某个类的实例只有一个,但也会带来一些缺点。例如,单例模式会增加代码的复杂度,可能会导致代码的可测试性降低,因此需要在使用单例模式时慎重考虑。