【Java多线程案例】单例模式

本文详细介绍了Java中的单例模式,包括饿汉模式和懒汉模式。饿汉模式在类加载时即创建实例,无线程安全问题,而懒汉模式在多线程环境下可能导致实例创建多次。为解决懒汉模式的线程安全问题,文章提出了通过同步锁(synchronized)和双重检查锁定(DCL,double-checkedlocking)配合volatile关键字来确保实例的唯一性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本期讲解单例模式的饿汉模式与懒汉模式,以及如何解决懒汉模式造成线程的不安全问题。

目录

什么是单例模式?

1. 饿汉模式

2. 懒汉模式

2.1 懒汉模式单线程版

2.2 懒汉模式多线程版

3.  解决懒汉模式不安全问题

3.1 保证原子性

3.2 防止指令重排序


什么是单例模式?

首先,单例模式是一种设计模式。何为设计模式,设计模式类似于固定的套路。例如考驾照的科目二项目,教练会总结出一些点位,因此我们按照这些点位去练习然后考试就能很顺畅的通过。在 Java 中也是如此,常见的就是开发中前辈设计好的一些案例,我们直接拿来用即可。

单例模式是在进程中有且仅有一份实例的模式,所以我们称之为单例。此外单例模式分为饿汉模式懒汉模式

通过上图,我们可以看到。thread1 - thread3 都共用 Singleton 这个实例,这样的一个模式就是单例模式。


1. 饿汉模式

看到饿汉二字,我们就会想到这是一种饥渴的状态,有一种一看到饭就冲上去吃的感觉。因此,饿汉模式它是一种类加载时就创建对象的一种模式,如下代码:

//自定义类singleton
class Singleton {
    //创建一个对象
    private static Singleton instance = new Singleton();
    //提供一个获取instance的方法
    private static Singleton getInstance(){
        return instance;
    }
}

当以上代码中的自定义类 singleton 被加载后,就会创建一个 instance的对象。这时候我们就可以通过一个获取 instance 对象的方法 getInstance 来使用这个实例。由于 singleton 类中的所有成员变量与成员方法都是被 private 修饰,因此达到了封装效果也体现出了单例模式的唯一性

饿汉模式强调一个饥渴,类一被加载就创建了一个对象。它不存在线程安全问题,当多个线程调用这个饿汉模式时得到的都是同一个实例,并不重新创建实例。

以上的饿汉模式,设计得还是有问题的。如果我们新建了一个实例,这样就不能保证饿汉模式是一个单例模式,如以下代码:

public static void main(String[] args) {
        //s1和s2都是同一个实例
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //s3新建了一个实例
        Singleton s3 = new Singleton();
    }

因此,我们必须保证在 Singleton 类不能被实例化,这时我们可以在 Singleton类 中提供一个被 private 修饰的构造方法,这样无论如何 Singleton 类都不能被 new 了。如下行代码:

private Singleton(){};

2. 懒汉模式

饿汉模式体现了一种饥渴,懒汉模式给人感觉就是一种懒散的状态,一碗饭在面前爱吃不吃的感觉。因此,懒汉模式在创建实例时并不在类加载时创建对象,而是什么时候需要创建对象了就去创建,不需要则不创建

举个例子,在家里面,吃午餐用了五个盘子,由于很懒没有及时的去洗。到了晚上,炒菜发现没盘子可用了才洗个盘子用来盛菜。剩余的四个盘子还是不洗,至于臭了还是烂了并不在意。这就是体现出懒汉模式中的“懒”状态。


2.1 懒汉模式单线程版

通过上方例子的讲解,我们可以了解到。懒汉模式在使用某个对象时,得判断该是否实例化。如果实例化过了就不创建直接返回该实例,没有则创建后返回该实例。如下流程图:

案例:通过懒汉模式创建一个自定义类 SingletonLazy ,并在 main 方法中创建两个 SingletonLazy 类的引用 s1、s2,使得 s1 等于 s2。因此,我们可以写出以下代码:

class SingletonLazy {
    //创建一个SingletonLazy的实例为空
    private static SingletonLazy instance = null;
    //获取该实例的方法
    public static SingletonLazy getInstance(){
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    //提供一个private修饰的构造方法,保证唯一性
    private SingletonLazy(){};
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        //s1和s2是同一个实例
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

运行后打印:

以上代码,与饿汉模式相比来说更符合与现实开发。当然,上述代码是单线程版的饿汉模式。因此是比较安全的,但是把以上代码应用到多线程情况下就会造成线程不安全问题。


2.2 懒汉模式多线程版

首先,我们来看下上文中创建的懒汉模式的 SingletonLazy 类的代码。

class SingletonLazy {
    //创建一个SingletonLazy的实例为空
    private static SingletonLazy instance = null;
    //获取该实例的方法
    public static SingletonLazy getInstance(){
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    //提供一个private修饰的构造方法,保证唯一性
    private SingletonLazy(){};
}

在多线程的学习中,我们以及知道了造成线程不安全有线程抢占资源这个概念,其原因就是多个线程在执行过程中进行读和写操作也就是修改操作,导致的不安全问题。

多线程下,懒汉模式会导致创建多个实例,因此不能保证实例的唯一性。假如有多个线程进行调用了 getInstance 方法。线程1在执行同时,由于运行速度过快,线程2也开始执行了,导致最后创建了两个实例。这样就不叫单例模式了。


3.  解决懒汉模式不安全问题


3.1 保证原子性

在上方创建的懒汉模式中的 if 语句 和 new 操作,是不具备原子性的。其原因为在多个线程调用 getIstance 这个方法。


上文中设计的懒汉模式预期的效果为:当多个线程调用 getInstance 方法后。第一个调用 getInstance 的线程会进行 new 操作创建一个 instance 实例,其他线程调用 getInstance 方法后发现 instance 不为 null 则不进行 new 操作。

但由于线程的抢占式执行,导致第一个调用 getIstance 的线程执行到第一步后,其他线程抢占执行了并调用了 getIstance 方法,这个时候两个线程 if 语句都判断 instance 等于 null 。这时候就创建了两个 instance 对象。

这样就导致 if 语句 和 new 操作就不具备原子性(不能完整的执行)。因此,我们可以使用 synchronized 关键字来加锁,使得这两个操作具备原子性。如下代码所示:

//获取该实例的方法
    public static SingletonLazy getInstance(){
        //给if语句和new操作加锁
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

当然,以上代码加上了锁虽然保证了 if 语句和 new 操作具备了原子性,但还不算是最优的写法。我们可以想象一下,每个线程调用 getInstance 这个方法时候,都会进行锁的竞争这样就会阻塞等待,这样的时间效率是非常低的。

因此,我们可以使用双重 if 语句来减少阻塞等待。如下代码:

public static SingletonLazy getInstance(){
        if (instance == null) {
            //给if语句和new操作加锁
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

以上的代码中两条 if 语句里面的条件是一样的,但其初心不同。第一条 if 是为了判断是否有 instance 这个实例不让其他线程进入锁的竞争,第二条 if 语句是在锁下进行判断的创建唯一一个实例。

当然,可能多个线程抢占并进入到了第一条 if 语句,但第一个进入锁的线程完成了创建实例任务后,其他线程进入锁后 if 判断的实例不为空也就不会再多创建实例了。

这样的设计才是懒汉模式的标准写法,保证了实例的唯一性。但有一极端的情况,指令被重排序了,具体请看下方讲解。


3.2 防止指令重排序

有一种极端的情况,两个线程同时调用了 getInstance 方法,都进入了第一条 if 语句里面。线程1进入了锁(synchronized)的范围,但由于指令重排序导致 new 这个操作与原本执行顺序不一致。这时候,线程2进入了锁的范围,发现 instance 实例已被创建,则返回 instance 实例。

这样就会导致线程2调用的构造方法是虚无的、不知道是哪里的,造成了线程的不安全。因此,我们在初始化 instance 实例时加上 volatile 关键字,使得指令能够按照顺序进行。

volatile private static SingletonLazy instance = null;

综合起来,创建一个懒汉模式的代码如下所示:

class SingletonLazy {
    //创建一个SingletonLazy的实例为空,volatile修饰保证指令顺序执行
    volatile private static SingletonLazy instance = null;
    //获取该实例的方法
    public static SingletonLazy getInstance(){
        //判断instance实例是否存在,存在则返回
        if (instance == null) {
            //给if语句和new操作加锁,防止多new操作
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    //提供一个private修饰的构造方法,保证唯一性
    private SingletonLazy(){};
}

总结:

  • 单例模式是在进程中有且仅有一份实例的模式。
  • 单例模式分为饿汉模式与懒汉模式。
  • 饿汉模式天然不存在线程不安全问题。
  • 懒汉模式存在线程不安全问题,因此需要进行加锁(synchronized)操作与防止指令重排序(volatile)操作。

🧑‍💻作者:一只爱打拳的程序猿,Java领域新星创作者,阿里云社区优质创造者。

🗃️文章收录于:Java多线程编程

🗂️JavaSE的学习:JavaSE

🗂️Java数据结构:数据结构与算法

 本篇博文到这里就结束了,感谢点赞、评论、收藏、关注~

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只爱打拳的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值