Java 多线程4——wait / notify方法的使用 + 单例模式(饿汉/懒汉)


前言

本人是一个刚刚上路的IT新兵,菜鸟!分享一点自己的见解,如果有错误的地方欢迎各位大佬莅临指导,如果这篇文章可以帮助到你,劳请大家点赞转发支持一下!

本篇文章讲解了多线程中常用的wait与notify方法,与软件开发的一种模式,单例模式,细细品读,你会为其中的细节着迷。


一、wait()与notify()方法

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。

假设有一个做饭程序

其中有三个线程,买菜线程,洗菜线程,炒菜线程。

这其中我们就希望这三个线程是按照

1️⃣先执行买菜线程。
2️⃣买菜线程执行完毕后,开始执行洗菜线程。
3️⃣洗菜线程执行完毕后,开始执行炒菜线程。

在多线程3该篇文章中,讲解了一个join()方法可以控制顺序,但是他的功效还是有限的,因此便提供了wait()与notify()方法。

wait()与notify()是Object类的方法,只要你不是内置类型与基本数据类型,都可以使用wait与notify。


wait()方法

wait的功能:

  • 使当前执行代码的线程进行等待。(把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常
锁对象必须与waite对象是同一个

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间)
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出InterruptedException异常

notify()方法

notify的功能:

  • notify 方法是唤醒同一个对象上等待的线程。
  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到"的规则)。
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

notify 要搭配 synchronized 来使用. 脱离 synchronized 使用 notify 会直接抛出异常


notifyAll()方法

notify方法只是唤醒某一个等待线程,使用notifyAll方法可以一次唤醒所有的等待线程。

【注意】

  • 虽然是同时唤醒 所有等待的线程, 但是这些线程线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行。

使用范例

wait()与notify()

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        // 创建锁对象
        Thread t1 = new Thread(() -> {
            System.out.println("t1线程开始执行");
            try {
                synchronized (locker) {
                    System.out.println("t1线程开始等待t2线程");
                    // 锁对象必须与waite对象是同一个
                    locker.wait();
                    // 让t1线程开始等待,直到其他线程中调用locker.notify()方法
                    System.out.println("t2线程执行完毕,t1线程开始执行");
                    System.out.println("t1线程执行完毕");
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(() -> {
            System.out.println("t2线程开始执行");
            System.out.println("t2线程执行完毕");
            synchronized (locker) {
                locker.notify();
                // 调用notify,唤醒使用locker.wait()方法
            }
        });
        t1.start();
        Thread.sleep(1000);
        t2.start();

    }

在这里插入图片描述


wait()与notifyAll()

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println("t1线程开始执行");
            synchronized (locker) {
                try {
                    System.out.println("t1等待t4");
                    locker.wait();
                    System.out.println("t1执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            System.out.println("t2线程开始执行");
            synchronized (locker) {
                try {
                    System.out.println("t2等待t4");
                    locker.wait();
                    System.out.println("t2执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t3 = new Thread(() -> {
            System.out.println("t3线程开始执行");
            synchronized (locker) {
                try {
                    System.out.println("t3等待t4");
                    locker.wait();
                    System.out.println("t3执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t4 = new Thread(() -> {
            System.out.println("t4线程开始执行");
            synchronized (locker) {
                System.out.println("t4执行完毕");
                System.out.println("唤醒所有等待线程");
                locker.notifyAll();
            }
        });
        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(1000);
        t4.start();
    }

在这里插入图片描述


二、单例模式

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种。


就以读小说来举例解释饿汉模式懒汉模式的区别

饿汉模式
饿汉模式,顾名思义,饿汉就会急切的想要吃饭,所以饿汉模式的特点就是很急,把能做的事都做完。

读小说,一次只能显示一页的内容,饿汉模式就会将整部小说都加载到内存当中,并显示一页内容。

懒汉模式

懒汉模式,顾名思义,懒汉很懒,只会做必须要做的事,非必要不做事。所以懒汉模式的特点就是只有当你真正用到的时候,我才会创造出来提供给你。

读小说,一次只能显示一页的内容,懒汉模式就会只加载一页的内容显示给用户,如果用户要翻下一页,那么再去加载。

单线程版的饿汉模式与懒汉模式

饿汉模式

class Singleton {
	// 唯一实例的本体
    private static Singleton instance = new Singleton();
    // 饿汉模式直接创建出了Singleton这个对象

    // 禁止外部 new 该对象
    private Singleton() {}

    // 获取到对象的方法
    public static Singleton getInstance() {
        return instance;
    }
}

懒汉模式

class SingletonLazy {
	// 唯一实例的本体
    private static SingletonLazy instance = null;
    // 懒汉汉模式不会直接创建Singleton这个对象

    // 禁止外部 new 该对象
    private SingletonLazy() {}

    // 获取到对象的方法
    public static SingletonLazy getInstance() {
        // 如果该对象为null,那么就创建对象
        if(instance == null) {
            instance = new SingletonLazy();
        }
        // 对象不为null,直接返回当前instance
        return instance;
    }
}

多线程版懒汉模式的BUG

饿汉模式在多线程的情况下,并不会有bug出现。
下面就重点讲解多线程版懒汉模式。

上一篇文章讲解过多个线程修改同一共享数据时出现的bug。
其实此处的bug也类似。


大前提:此时instance为null

  1. 有两个线程t1与t2。
    此时,t1线程执行到了if语句,判断instance == null 为true,将要执行下方一行代码。
    instance = new Singleton();
    此时,CPU被调度去执行t2线程了。
    (提醒,这里创建对象的语句并未执行,因此instance仍为null)

在这里插入图片描述


  1. 此时t2线程也执行到了if语句,此时的instance仍为null,因此t2线程得到了一个新对象。
    CPU再调度去执行t1,此时t1也会得到一个新对象,且这两个对象不是同一个,因此两个线程就得到了两个实例,就不能称之为单例模式。

在这里插入图片描述

这就是多线程版懒汉模式由线程抢占式执行导致的BUG。


还有一个指令重排序导致的BUG。

创建一个对象分为三步:
1️⃣创建内存、。
2️⃣调用构造方法、。
3️⃣把内存地址,赋给引用对象、。

有极小的概率,执行顺序会从1️⃣2️⃣3️⃣经过编译器的指令重排序编程1️⃣3️⃣2️⃣。
如果在多线程情况下,执行完1️⃣3️⃣,此时引用对象已经创建好了,还没来得及创建里面的构造方法。
系统就把CPU调度给其他线程了,其他线程再识别instance就不是null,就会返回instance,如果此时调用instance中的方法,就会出bug。

解决BUG

针对线程抢占式执行导致的BUG,只需要加一个锁,便可以解决。
针对指令重排序导致的BUG,只需要使用volatile关键字修饰instance变量即可。

class SingletonLazy {
    private static volatile SingletonLazy instance = null;
    // 懒汉汉模式不会直接创建Singleton这个对象

    // 禁止外部 new 该对象
    private SingletonLazy() {}

    // 获取到对象的方法
    public static SingletonLazy getInstance() {
        // 如果该对象为null,那么就创建对象
        synchronized (SingletonLazy.class) {
            // SingletonLazy.class得到调用该方法的对象
            if(instance == null) {
                instance = new SingletonLazy();
            }
        }
        // 对象不为null,直接返回当前instance
        return instance;
    }
}

上述代码还可以优化。

上述加锁之后,无论instan是否为null,只要执行getInstance(),就一定会进入该锁,其他线程只能等待。

所以我们可以再加一个if语句,来判断他是否为null,

如果为null那么进入该锁,创建对象,为了线程安全,其他线程等也就等了。
如果不为null,那么线程就直接返回当前的instance,此时既保证了线程安全,也提高了效率。

class SingletonLazy {
    private static volatile SingletonLazy instance = null;
    // 懒汉汉模式不会直接创建Singleton这个对象

    // 禁止外部 new 该对象
    private SingletonLazy() {}

    // 获取到对象的方法
    public static SingletonLazy getInstance() {
        // 如果该对象为null,那么就创建对象
        if(instance == null) {
            synchronized (SingletonLazy.class) {
                // SingletonLazy.class得到调用该方法的对象
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        // 对象不为null,直接返回当前instance
        return instance;
    }
}

上述的两个if语句,缺一不可,大家可以去掉一个语句,在脑海中模拟一下,看看是否还会存在BUG。

如果有老铁不明白,可以评论区或私信我。


总结

以上就是今天要分享的内容了,多线程的内容,既危险又迷人,这也是以后工作中一定会用到的内容,同志们加油吧!!

路漫漫,不止修身也养性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值