饿汉式:
概述:
1.饿汉式是单例模式中最简单、最直接的一种实现方式。
2.它的核心思想在于:“我饿了,我现在就要吃!” —— 也就是说,不管程序需不需要,我都
先在类加载的时候就把这个单例实例创建好,等着你来用。
3.这种“急切”的初始化方式,因此得名“饿汉式”。
代码及注释解释:
就是一上来先创建实例对象,构造器私有,然后通过方法return这个实例对象
public class EagerSingleton {
// 1. 静态常量:在类加载阶段,就由JVM初始化这个唯一实例。
// 关键字 `final` 可选,但可以明确表示此实例不可改变,增强可读性。
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 2. 私有构造函数:防止外部通过 `new` 关键字创建实例。
private EagerSingleton() {
// 防止在反射攻击下被多次实例化(一种简单的防护,但并非绝对安全)
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("EagerSingleton is initialized!");
}
// 3. 全局静态访问点:提供给外部获取这个唯一实例的方法。
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
工作原理
饿汉式的线程安全性并不是通过同步代码(synchronized) 来实现的,而是利用了 JVM 的类加载机制。
JVM 在加载一个类时,会初始化其静态变量(
static
)。这个过程是线程安全的,因为 JVM 保证了每个类只会被加载一次,其静态变量自然也只会被初始化一次。
因此,
INSTANCE
这个单例对象在类加载的初始化阶段就被创建出来了,后续所有调用getInstance()
的方法都只是返回这个早已创建好的对象的引用。
优缺点:
优点
实现简单:代码非常简洁明了,易于理解。
线程安全:无需任何同步措施,由 JVM 底层机制保证线程安全,性能无忧。
缺点
可能造成资源浪费:这是饿汉式最大的问题。如果这个单例实例的构造过程很耗时(比如加载大量数据、连接资源),或者这个实例在程序运行过程中根本就没被用到,那么它的提前初始化就是一种不必要的内存和计算资源的浪费。
非懒加载 (Lazy Loading):无法做到“用时再创建”,不符合一些特定场景的需求
适用场景
内存占用小:实例本身占用的内存资源不大,即使不用,也能接受其常驻内存。
几乎每次运行都会用到:你几乎可以肯定这个实例在程序运行中一定会被使用到。
追求极致的性能:在超高并发环境下,虽然双重检查锁(DCL)性能也很好,但饿汉式的
getInstance()
方法没有任何同步开销,理论上是最快的。
懒汉式:
.核心概念
1.懒汉式的核心思想是:“我很懒,只有到你真正需要我的时候,我才会动手创建自己。”
2.这与饿汉式的“迫不及待”形成鲜明对比。它避免了提前初始化可能造成的资源浪费,只有在
第一次调用 getInstance()
方法时才会创建实例。
3.这种“用时再创建”的方式也称为,懒加载,延迟加载 (Lazy Loading)。
版本推演以及代码实现
版本1(线程安全问题):
这是最原始的版本,但是面临线程不安全的问题,
假设线程1进入if语句后,还没有执行完创建对象实例的操作
此时线程2也进入了判断就出现了线程冲突
public class UnsafeLazySingleton {
private static UnsafeLazySingleton instance;
private UnsafeLazySingleton() {}
public static UnsafeLazySingleton getInstance() {
// 线程不安全的关键点!
if (instance == null) { // 【步骤1】:线程A和线程B同时检查,都发现instance为null
instance = new UnsafeLazySingleton(); // 【步骤2】:它们都会执行这行代码,导致创建多个实例
}
return instance;
}
}
版本二(解决线程安全 但存在效率问题):
加上关键字后 表明在线程1没执行完时线程2不会进入,会等待
但是这表明,在同一时间内只能有一个线程执行,效率大打折扣,所以也不推荐
public class SynchronizedLazySingleton {
private static SynchronizedLazySingleton instance;
private SynchronizedLazySingleton() {}
// 使用synchronized关键字修饰整个方法
public static synchronized SynchronizedLazySingleton getInstance() {
if (instance == null) {
instance = new SynchronizedLazySingleton();
}
return instance;
}
}
版本三(双重检查锁)
此时将关键字放在了最外层判断里面,就算有线程同时进入判断也会先等待,
等前一个创建完后在进行判断,不会重复创建实例,
而其他线程直接在最外层判断被拦截,相比于版本二极大的提高了效率
public class DCLSingleton {
// 关键:使用volatile关键字禁止指令重排序
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
// 第一次检查:避免不必要的同步
if (instance == null) {
// 同步代码块
synchronized (DCLSingleton.class) {
// 第二次检查:确保在同步块内再次判断,防止多次创建
if (instance == null) {
instance = new DCLSingleton(); // volatile 关键字在此处至关重要!
}
}
}
return instance;
}
}
注:在声明instance时为什么需要volatile??
语句
instance = new DCLSingleton();
并不是一个原子操作,它大致分为三步:为对象分配内存空间
初始化对象(调用构造函数等)
将
instance
引用指向分配的内存地址
JVM 可能会进行指令重排序,将步骤2和步骤3调换顺序。如果线程A执行了1和3,但尚未执行2,此时
instance
已不为null
(但对象未初始化)。此时如果线程B执行第一重检查
if (instance == null)
,会发现instance
不为null
,于是直接返回这个半成品对象,从而导致程序出错。volatile
关键字的作用就是禁止JVM对此语句进行指令重排,从而保证线程安全。
版本四(静态内部类):
这被称为最优雅的懒加载
利用了jvm的机制,在加载外部类时,内部类不会被加载,只有在调用方法时,
才开始加载静态内部类,这个过程是线程安全的,同时效率也提升了。
public class InnerClassSingleton {
private InnerClassSingleton() {}
// 静态内部类
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举的方式:
核心概念
1.用枚举实现单例模式,可以说是 《Effective Java》 这本书作者 Joshua Bloch 提倡的、
2.目前实现单例的最佳实践。它的核心思想是:利用Java枚举类型本身的特性,让JVM来为
我们保证单例的绝对唯一性,并防御各种攻击。
代码实现及使用(及其简单)
public class SingletonTest3 {
public static void main(String[] args) {
Singleton1 instance1 = Singleton1.INSTANCE;
instance1.say();
}
}
enum Singleton1 {
INSTANCE;
public void say() {
System.out.println("ok");
}
}
java中使用单例模式的源码例子:
在Runtime类中可以看出,它使用了饿汉式: