当前位置: 首页 > news >正文

Java单例模式中的饿汉模式和懒汉模式

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. 申请内存空间(类比:付钱买房子)
  2. 在空间上构造对象(类比:房子装修)
  3. 内存空间的首地址,赋值给引用变量(类比:拿到钥匙)

在指令重排序的情况下,这三个步骤的执行顺序可能会发生变化,例如出现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;
    }
}

通过对饿汉模式和懒汉模式的深入剖析,我们全面了解了单例模式的实现方式及其在不同场景下的应用。无论是饿汉模式的急切创建,还是懒汉模式的延迟加载,都为我们在软件开发中实现类实例的唯一性提供了有力的工具。在实际项目中,我们应根据具体的需求和场景,灵活选择合适的单例模式实现方式,以构建出更加健壮、高效的软件系统。

相关文章:

  • 使用ES支持树状结构查询实战
  • SQLite 查询数据库属性
  • 一道积分_3
  • Linux多线程
  • Java 大视界 -- 基于 Java 的大数据机器学习模型的可扩展性设计与实践(149)
  • 使用 Python 开发 MCP Server 及 Inspector 工具详解
  • 【2025 华中师范大学-菜鸟杯程序设计竞赛】部分题解
  • 多模态工作idea讨论
  • HTTPS协议—加密算法和中间攻击人的博弈
  • 【Dive Into Stable Diffusion v3.5】2:Stable Diffusion v3.5原理介绍
  • Baklib驱动企业数字化内容管理升级
  • WebSocket接入SSL证书
  • Java面试黄金宝典11
  • 数据库锁机制
  • 《Oracle DBA入门实战:十大高频问题详解与避坑指南》
  • 智能飞鸟监测 守护高压线安全
  • 使用 Go 构建 MCP Server
  • 【yolo11自定义实例分割训练集教程】
  • 2.2.盈亏平衡分析
  • webstorm调试模式报错:Cannot detect a launch configuration
  • 外媒:哈马斯一名高级指挥官尸体被发现,系辛瓦尔弟弟
  • 林诗栋/蒯曼混双取胜,国乒赢得多哈世乒赛开门红
  • 新城市志|GDP万亿城市,一季度如何挑大梁
  • 湖南慈利一村干部用AI生成通知并擅自发布,乡纪委立案
  • 大陆非遗项目打铁花、英歌舞将在台演出
  • 中国情怀:时代记录与家国镜相|澎湃·镜相第三届非虚构写作大赛暨七猫第六届百万奖金现实题材征文大赛征稿启事