多线程之单例模式 - 懒汉模式 + 饿汉模式实现

本文介绍了Java中的wait与notify方法在控制线程顺序执行中的作用,以及它们与sleep方法的区别。接着详细讲解了单例模式的概念,通过饿汉模式和懒汉模式展示了如何在Java中实现单例,并分析了懒汉模式在多线程环境下的线程不安全问题及其解决方案,包括加锁、双重检查锁定和使用volatile关键字来确保线程安全。

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


前言

本篇介绍的是wait与notify方法,通过wait来顺序控制执行一些代码,了解单例模式,进行单例模式的简单实现,介绍饿汉模式下出现线程不安全的问题与解决;如有错误,请在评论区指正,让我们一起交流,共同进步!



本文开始

1. 认识 wait 与 notify

为什么要有wait 与 notify ?
线程调度是无序的,但是一定场景下,希望线程是有序执行的,这就可以使用wait 和 notify;

之前了解了,join能够等待,来控制顺序,但是其功效有限;而wait也是等待的作用,但又不相同;

1.1 wait,notify的使用

wait: 发现条件不满足 / 时机不成熟,就会阻塞等待;
notify: 其他线程构造了一个成熟的条件,就可以唤醒等待线程;

例如:
A去ATM取钱,A进去取钱,发现ATM中没有钱,A就会wait释放锁并阻塞等待,当另外的线程,把ATM冲上钱,就可以唤醒A,让A进行取钱操作;

wait与notify前提使用条件:
wait 和 notify 是Object 方法,只要是类对象,就可以使用wait,notify;

1.2 使用 wait 必须加锁

不加锁代码:

    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        object.wait();//中断就会报异常
    }

结果报错:

在这里插入图片描述

【注】illegal Monitor State Exception: 非法的,监视器(synchronized: 监视器锁),状态,异常;=》非法的锁状态异常;

object.wait() : wait方法做三件事
① 解锁: 这就说明,使用wait必须加锁(必须写的synchronized代码块中),才能解锁;
【注】加锁对象必须和wait的对象是同一个
② 阻塞等待
③ 当收到通知的时候,就唤醒,同时尝试重新获取锁;

join 与 wait 的区别:
前提有两个线程t1,t2;
① join等待一定是串行的,只能t2先执行完,再继续执行t1;
② wait和notify, 中的wait只是等待线程中的一部分;可以先让t2执行完一部分,再执行t1, t1执行一部分,t2再继续执行;(可以调整执行的顺序)

wait 使用的两种方式
使用wait会阻塞等待让线程进入WAITING状态;
带参数 的wait(时间):带参数的,指最大等待时间;(等到最大时间,没人通知,就会自己唤醒自己)
不带参数 的wait : 会一直等待;(只能等待被唤醒或者被中断)

1.3 notify 也需要加锁

前提:必须先执行wait, 然后才能有notify; (不然没锁,怎么解锁) 虽然没有唤醒wait,但是不会让代码出现异常;

notifyAll : 唤醒多个线程;
【注】如果有多个线程调用wait,notify只能随机唤醒一个;而notifyAll 可以把线程全部唤醒,此时多个线程重新竞争锁,再依次执行;

1.4 wait 与 sleep 的区别?

① wait用于线程通信,sleep用于阻塞线程;
② 设计的初心不同,wait解决线程之间顺序控制;sleep只能让当前线程休眠一会;
③ wait 是Object的方法,sleep是Thread的静态方法;
④ wait使用需要配合synchronized使用,sleep不需要;

2.单例模式

什么是单例模式?
单例模式,是一种经典的设计模式;类似于玩游戏的攻略 / 下棋的棋谱 ,有一些固定的招式;

2.1 单例模式的介绍

单例:指单个实例,一个程序中,某个类,只能创建出一个实例(对象),不能创建多个对象;

java中的单例模式,借助java语法,保证某个类,只能创建出一个实例,而不能new多次;

2.2 java语法中如何实现单例模式

这里介绍两种方式:
饿汉模式
从容的状态:比如吃饭洗碗,吃完饭马上洗碗;
懒汉模式
急迫的状态:比如吃饭洗碗,吃完饭,先不着急洗碗,等下一次用的再洗碗;

例如计算机读取硬盘中的文件内容,并显示;
饿汉:把文件所有内容全部读到内存中,并显示;
懒汉:只读取文件中的一部分,能填充当前屏幕,如果翻页,再读取其他文件内容,如果不翻页,就不用再读;

2.3 实现单例模式

此处把Singleton设置为单例的

2.3.1 饿汉模式实现单例

饿汉模式:开始直接创建实例,不管用不用;
【注】
① 唯一实例本体使用static修饰
② 获取实例方法提供get方法
③ 禁止外部再创建实例 使用private修饰无参构造(private修饰必须提供get方法来获取实例)

class Singleton {
    //唯一实例本体
    private static Singleton instance = new Singleton();
    //被static修饰,该属性是类的属性 =》 类对象
    //JVM 中,每个类的类对象只有唯一一份,类对象的成员也是唯一一份

    //private修饰,需要get方法,外部才能获取到
    //获取到实例的方法
    public static Singleton getInstance() {
        return instance;
    }
    //禁止外部new 实例
    private Singleton() {

    }
}

测试代码:

 public static void main(String[] args) {
        //此时singleton1,singleton2调用的是同一个对象
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();

        //为了防止new一个新的对象,给无参构造用private修饰
        //Singleton singleton3 = new Singleton();//此时不能new了
    }

2.3.2 懒汉模式实现单例模式

懒汉模式:开始不创建实例,等到使用的时候才会创建;

class SingletonLazy {
    //不马上创建实例
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
        //等到调用的时候,才创建实例
        if(instance == null) {
            instance = new SingletonLazy();
        }
        //如果有了实例直接返回,也保证了只要一个实例
        return instance;
    }
    private SingletonLazy() {}
}

测试:

    public static void main(String[] args) {
        SingletonLazy singletonLazy1 = SingletonLazy.getInstance();
        SingletonLazy singletonLazy2 = SingletonLazy.getInstance();
        //比较singletonLazy1 == singletonLazy2,相等就是同一个对象
        System.out.println(singletonLazy1 == singletonLazy2);
     }

2.3.3 懒汉模式下产生的线程不安全

通过上述两个方式实现单例模式,它们是线程安全的吗?
可以通过分析线程不安全产生的原因来判断;

饿汉模式:获取实例方法,只是读操作,不会修改变量,线程是安全的;
懒汉模式:多线程下,无法保证创建对象的唯一性, 线程是不安全的;

懒汉模式下两个线程调用getInstance:在这里插入图片描述

问题1 产生原因:
t1线程先执行 if 判断,准备创建实例但还没有创建,此时t2 进行 if 判断,进入代码块也准备拆改那就实例,这就造成创建多个实例的现象;=》相当于多个线程修改同一个变量了;
( if 和 new不是原子的,会造成创建多个实例对象;创建过多会非常占内存空间,导致线程不安全;)

解决问题1方式:
加锁 保证 if 和 new 的原子性;
【注】加锁不一定线程安全,必须保证锁加对位置;

代码实现:

 public static SingletonLazy getInstance() {
        //给当前类对象加锁
        synchronized (SingletonLazy.class) {
            if(instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

问题2:
每次调用genInstance都会触发锁竞争,让加锁操作变得低效;

通过问题可以发现,懒汉模式的线程不安全,只出现在首次创建对象,一定对象创建完毕,后续调用getInstance,就只是读操作,就不会产生线程安全;=》只给第一次加锁即可;

解决问题2 方式:使用双重 if 解决
① 外层:判断是否要加锁
② 内层:判断是否创建对象

代码实现:

public static SingletonLazy getInstance() {
        //判断是否要加锁,如果有对象就不必加锁,此时是线程安全的
        //外层判断是否要加锁
        if(instance == null) {
            synchronized (SingletonLazy.class) {
                //等到调用的时候,才创建实例
                //里层判断是否创建对象
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        //如果有了实例直接返回,也保证了只要一个实例
        return instance;
    }

问题3:
多线程调用getInstance 进入操作会遇到new SingletonLazy操作, 创建实例会产生指令重排序问题;

创建实例一般有三步操作:
① 创建内存
② 调用构造方法 (初始化)
③ 把内存地址,赋给引用
原因:但是②③顺序不能够确定,假设两个线程t1,t2; t1可能先执行③顺序操作,此时t2执行 if判断,发现instance不为空,直接返回,而t1没有初始化变量等,t2只是拿到了不完整的实例对象,直接使用就会造成线程安全
【注】上述指令重排序发生的概率很小,但不能忽视;

解决问题3 :使用volatile 禁止指令重排序

    volatile private static SingletonLazy instance = null;

最后完整的代码:

public static SingletonLazy getInstance() {
		//使用volatile: 防止指令重排序
   		volatile private static SingletonLazy instance = null;
        //外层判断是否要加锁
        if(instance == null) {
            synchronized (SingletonLazy.class) {
                //等到调用的时候,才创建实例
                //里层判断是否创建对象
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        //如果有了实例直接返回,也保证了只要一个实例
        return instance;
    }
2.3.4 小结懒汉模式下产生的线程不安全

解决懒汉模式下的线程不安全方式:
① 加锁把 if 和 new操作变成原子的;
② 双重 if 减少不必要的加锁操作;
③ 使用 volatile 禁止指令重排序,保证后续线程能拿到完整对象;


总结

✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值