Java单例模式中的饿汉模式和懒汉模式
- 一、单例模式的显著特点
- 单一实例
- 全局访问
- 二、饿汉模式:急切的实例创建者
- 三、懒汉模式:延迟的实例构建者
- 1. 不考虑线程安全的初始版本
- 2. 引入同步机制解决线程安全问题
- 3. 优化性能:避免重复进入同步块
- 4. 使用volatile关键字彻底解决线程安全隐患
一、单例模式的显著特点
单一实例
在整个应用的运行过程中,无论在何处、以何种方式尝试创建某一个类的实例,最终得到的都将是同一个对象。这就好比在一个公司中,总经理只有一位,无论各个部门通过何种渠道去联系总经理,对接的都是这唯一的一位负责人。这种唯一性避免了因创建多个相同类实例而可能导致的资源浪费、数据不一致等问题。
全局访问
单例模式贴心地提供了一个全局可用的访问点。这意味着在应用程序的任何角落,只要有需求,相关代码都能够轻松地获取到这个单例实例。就如同在一个大型商场中,统一的客服中心作为全局访问点,各个店铺、顾客都能方便地联系到它,获取所需的服务或信息。通过这种全局访问机制,极大地提高了代码的交互性和协作性。
实现单例模式的方式丰富多样,而其中饿汉模式和懒汉模式尤为常见,接下来让我们深入探究这两种模式的奥秘。
二、饿汉模式:急切的实例创建者
饿汉模式,从名字便能直观地感受到它对于获取实例的急切渴望。这种模式在类被加载到内存的那一刻,就迫不及待地创建好了单例实例。由于实例的创建是在类加载阶段完成的,而类加载过程由 JVM 严格管控,天然具备线程安全性,因此不存在线程安全方面的困扰。以下是其简洁明了的代码实现:
class SingletonEager {
// 在类加载时就创建单例实例
private static final SingletonEager INSTANCE = new SingletonEager();
// 私有构造函数,防止外部通过 new 创建实例
private SingletonEager() {}
// 提供全局访问点
public static SingletonEager getInstance() {
return INSTANCE;
}
}
为了更好地理解饿汉模式在实际场景中的应用,我们以创建一个代表“狗”的单例对象为例:
public class Hungryman {
//单例模式——饿汉模式
//首先创建一个对象,直接构建实例
//必须是static静态吗???必须
private static String name;
private static int age;
private static Hungryman hungryman = new Hungryman(name,age);
//构造方法
private Hungryman(String name,int age){
this.name=name;
this.age=age;
}
//获得实例
public static Hungryman getHungryman(){
return hungryman;
}
}
在上述代码中,Hungryman类在加载时就创建了hungryman实例,后续任何地方调用getHungryman方法,获取到的都是同一个hungryman对象。
三、懒汉模式:延迟的实例构建者
与饿汉模式形成鲜明对比的是懒汉模式。正如其名,懒汉模式就像一个慵懒的工匠,只有当真正需要使用实例时,才会着手去创建。这种模式虽然在一定程度上实现了资源的按需使用,提升了资源利用效率,但也引入了潜在的线程安全问题。
1. 不考虑线程安全的初始版本
让我们先来看一下不考虑线程安全问题时,懒汉模式的简单实现:
public class Lazy {
//单例模式——懒汉模式
//定义一个对象为空
private static volatile Lazy lazy=null;
//构造方法
private Lazy(){}
//获得实例
public static Lazy getLazy(){
//如果lazy为空,未创建实例,则创建
if(lazy==null){
lazy=new Lazy();
}
//已有实例,则返回
return lazy;
}
}
在这段代码中,getLazy方法首先检查lazy是否为null,若为null则创建一个新的Lazy实例。然而,在多线程环境下,这种简单的实现方式可能会出现多个线程同时判断lazy为null,进而创建多个实例的情况,这显然违背了单例模式的初衷。
2. 引入同步机制解决线程安全问题
为了解决上述线程安全问题,我们可以通过加锁的方式,确保在同一时刻只有一个线程能够创建实例。具体实现如下:
synchronized (Lazy.class){
if(lazy==null){
lazy=new Lazy();
}
}
//已有实例,则返回
return lazy;
在这段代码中,使用Lazy.class作为锁对象,当一个线程进入同步块时,其他线程只能等待。这样就保证了在多线程环境下,Lazy类的实例不会被重复创建。
3. 优化性能:避免重复进入同步块
虽然加锁解决了线程安全问题,但每次调用getLazy方法都进入同步块会带来一定的性能开销。为了提高效率,我们可以在进入同步块之前先进行一次检查,若lazy已经不为null,则直接返回实例,无需进入同步块。优化后的代码如下:
if(lazy==null){
synchronized (Lazy.class){
if(lazy==null){
lazy=new Lazy();
}
}
}
//已有实例,则返回
return lazy;
通过这种双重检查的方式,在保证线程安全的前提下,尽可能地减少了同步带来的性能损耗。
4. 使用volatile关键字彻底解决线程安全隐患
尽管通过双重检查加锁的方式在很大程度上解决了线程安全问题,但在某些极端情况下,仍然可能出现问题。这是因为lazy = new Lazy()这一操作在 JVM 中并非原子操作,它实际上包含了三个步骤:
- 申请内存空间(类比:付钱买房子)
- 在空间上构造对象(类比:房子装修)
- 内存空间的首地址,赋值给引用变量(类比:拿到钥匙)
在指令重排序的情况下,这三个步骤的执行顺序可能会发生变化,例如出现1 -> 3 -> 2的顺序。这就可能导致一个线程在lazy引用已经指向内存空间,但对象尚未完成构造时,就返回了该引用,从而引发错误。
为了解决这个问题,我们可以使用volatile关键字修饰lazy变量。volatile关键字能够确保变量的可见性,禁止指令重排序,从而彻底解决线程安全隐患。最终完善的懒汉模式代码如下:
public class Lazy {
//单例模式——懒汉模式
//定义一个对象为空
private static volatile Lazy lazy=null;
//构造方法
private Lazy(){
}
//获得实例
public static Lazy getLazy(){
//如果lazy为空,未创建实例,则创建
//线程安全问题:加锁
//提高效率,当已知lazy不为空时,直接返回,不用进入锁中
//避免内存可见性问题和指令重排序问题,使用volatile定义lazy
if(lazy==null){
synchronized (Lazy.class){
if(lazy==null){
lazy=new Lazy();
}
}
}
//已有实例,则返回
return lazy;
}
}
通过对饿汉模式和懒汉模式的深入剖析,我们全面了解了单例模式的实现方式及其在不同场景下的应用。无论是饿汉模式的急切创建,还是懒汉模式的延迟加载,都为我们在软件开发中实现类实例的唯一性提供了有力的工具。在实际项目中,我们应根据具体的需求和场景,灵活选择合适的单例模式实现方式,以构建出更加健壮、高效的软件系统。