多线程(五):wait 和 单例设计模式

简介: 多线程(五):wait 和 单例设计模式

前提回顾


在开始讲解单例设计模式之前,先来复习先前的知识。


上一章一共讲了以下内容:


1. 给了一个线程不安全的例子(两个线程各自增 5w 次,结果为一个小于 10w 的随机数)


2. 线程抢占式执行,执行到任何一行都可能跳出去执行其他线程的代码。


3. 多个线程同时修改一个变量


4. 修改操作不是原子的


5. 内存可见性


6. 指令重排序


对此的解决方式就是:加锁,也就是将其写在 synchronized 代码块内部。


某个线程中的某个对象调用了  synchronized 代码块 中的代码,就会照成其他线程的 阻塞。


wait 和 notify


已经写到 第五章了,我们从第二章开始就一直说线程调度是无序的,但是总有情况是想要我们写一个有序的代码,当然我们之前学过一个 方法: sleep() ,是的,这个方法可以让线程睡眠,想要一个有序的程序,那么就只能一直睡了,这就相当于写一个 单线程了,效率并没有那么快。


🌰(栗子)  


73914cb0a44e4721ab459d97ab6d8bd8.png


对于上述情况,我们需要有个人通知他,老铁 ATM 没钱,等到 ATM 中有钱了再来通知他一次,让 老铁再去 抢占。


这里就换成 方法就是 wait 和 notify 。


当第一次发现 ATM 没钱时就 wait 来告诉他, 这里没钱了,让 滑稽 1 号休息一会,等到 notify 通知他的时候,滑稽 1 号就醒过来了,继续去 ATM 机取钱。


涉及到的方法:


wait() / wait(long timeout): 让当前线程进入等待状态.(带参的方法等会再说)

notify() / notifyAll(): 唤醒在当前对象上等待的线程. (notifyAll等会再说)


注意:

wait 和 notify 都是Object方法,只要你是类对象都可以调用 wait 和 notify

但是 内置类型(基本数据类型)不可以。

我们来写写看这个代码:


2b8ecc97e079409eb38ce93a2ef6f0ca.png

注意,任何可能照成线程阻塞的都需要抛 异常:

InterruptedException

我们来运行一下:


c35611ef2d6042f6adbc07df2b2e5f68.png


我们来翻译一下报的异常:非法的 监视(这里指的是synchroniezd 监视器锁) 状态异常。


这个异常主要是告诉我们 需要搭配 synchroniezd 来使用。


这里的 wait 主要做三件事:


1. 解锁


2. 阻塞等待


3. 当收到通知时,就唤醒,同时尝试重新获得锁。


注意加锁的对象要和 wait 的对象是同一个。


并且,notify 也要放在 synchroniezd 中使用。


来看看代码:


8ebe5053817b43aa9b4e7b5f09fa82ba.png

运行一下:

5568c85c485c4fe2b47b308b7abcd600.png


达到我们想要的效果了。

注意了 notify 必须要在 wait 之后 执行,才有效;如果反了,不会错,但是没有效果。

此时代码 不会被唤醒 ,不会产生其他异常。


283fcd37a6874e26b79d1b5caacbfa4b.png

上述代码中,虽然是 t1 先执行的,但是我们可以通过 方法来控制代码执行顺序,这就是 wait 和 notify 的功劳。

我们上面还提到了 wait 带参的方法。


wait 带参方法和 notifyAll 方法


这个参数就和 sleep 方法的参数一样,给定一个时间,在时间范围内 wait 被唤醒了就和 wait 方法一样,但是时间已经到了,wait 方法还是没有被唤醒,那么他就自己醒了。

但是无参的就是死等,等不到就一直等,倔强。


这里就相当于设了一个闹钟,时间到了被闹钟吵醒,时间没到自己就醒了。


这里就相当于设了一个闹钟,时间到了被闹钟吵醒,时间没到自己就醒了。


我们使用了wait 线程就进入到了 WAITING 状态;我们wait 的初心就是让线程进入阻塞状态。


上面还有一个 notifyAll 方法,看名字就知道这个是用于唤醒所有的线程的,这个用的比较少,我们了解一下即可。


wait 和 sleep 的比较


其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,

唯一的相同点就是都可以让线程放弃执行一段时间


当然为了面试的目的,我们还是总结下:

1. wait 需要搭配 synchronized 使用. sleep 不需要.

2. wait 是 Object 的方法 sleep 是 Thread 的静态方法


二者初心不同:


sleep 单纯的想让线程睡一会,wait 解决线程之间的顺序控制。


带参的 wait 和 sleep 都能被提前唤醒。


单例设计模式


首先介绍一下什么叫做设计模式:


设计模式就相当于 软件开发中的棋谱,通过前辈对一些常见的场景,总结出的代码编写套路。

单例的意思就是说只能创建一个对象,不允许创建多个对象。


我们从语法上如何做出单例模式的实现。


我们要写的话,可以写出 5 ~ 6 种方法;但是我们只讲两种(在校招种一般只考这两种):饿汉式、懒汉式。


饿汉模式(急迫)


懒汉模式(从容)


举个很简单的例子,从硬盘读取内容到显示器上,有两种方式,一种是要等一会,把所有的数据全都读到显示器上,还有一种是立即显示,一点一点读,前者是饿汉模式,后者就是懒汉模式,两者相比,当然是懒汉模式更高效了


饿汉模式


从上述例子看来,我们需要从一开始就 new 出一个对象,并且因为是单例,后面不允许再 new 出新对象。


那么我们可以在类的内部 将 构造器私有化,同时在内部实例化一个对象 ,指定为唯一的对象。


看代码:

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}


这里的 instance 被 static 修饰,就是类的属性,在 JVM 中每个类对象只有唯一一份,那么这个属性也是唯一一份。

并且我们将构造方法 私有化,外部无法调用 这个构造方法,再提供一个 获取类对象方法。

只能通过 get 方法去获取 对象的引用。


7f6d898eccb7418f9cd33c5eafd8b3a1.png


如上图。

这样 单例饿汉模式就写好了。

我们每次都只是读取这个实例化对象 ,只读不会对照成线程安全问题。


懒汉模式


非必要,不创建;能不 new ,就不 new 。

我们来实现一个代码:

class SingletonLazy {
    private static SingletonLazy instance = null;
    private SingletonLazy() {}
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

我们先将唯一对象 设为空,只当我们需要调用 获取到实例对象引用时才 给他 new 出新对象;否则一直为空。


b97f4667ebb94003a12084351204bf4a.png

我们在单线程下不必考虑线程安全问题,但是在多线程下必须要考虑到线程安全问题。

这里设计到了 " 写 " 的操作,那么必然涉及到了线程安全问题。

在 getInstance 方法中 ,从 if 到 return 这一整段不是原子的,可能会产生如下情况


30e0a8f31ffc410195eb134b0876bf48.png


看似只是多 new 了几次,如果我们 每个对象占有 100G 的内存呢,多 new 几次,后果不可估量!!!!

所以我们必须得对这段代码进行修改。

我们可以对其进行加锁操作

为了保证这个方法是原子的,可以直接选择对这个方法加锁:

5b23889e71c3430d82b98df840fb30c7.png


但是这样一来,会降低整个访问的速度,而且每次都要判断。那么有没有更好的方式来实现呢?


双重检查加锁


可以使用"双重检查加锁"的方式来实现,就可以既实现线程安全,又能够使性能不受到很大的影响。那么什么是"双重检查加锁"机制呢?


所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。


双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。


class SingletonLazy3 {
    private static volatile SingletonLazy3 instance = null;
    private SingletonLazy3() {}
    public static SingletonLazy3 getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new SingletonLazy3();
                }
            }
        }
        return instance;
    }
}


ed50fe8a5e7f4e9f9819caa3e67156f3.png


总结(保证懒汉式线程安全的方法):


1.加锁,保证if和new是原子的

2.双重if判定,防止不必要的加锁

3.加volatile关键字,禁止指令重排序,保证后面的线程拿到的是完整的对象

饿汉模式:是天然线程安全的,涉及到读操作

懒汉模式:不安全,需要操作把它边安全


单例设计模式就到这里,下一章继续多线程的其他案例。

相关文章
|
4天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
21 9
|
7天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
21 3
|
22天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
15 1
|
22天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
23 1
|
22天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
33 1
|
22天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
24 1
|
2月前
|
设计模式 存储 安全
设计模式——设计模式介绍和单例设计模式
饿汉式(静态常量)、饿汉式(静态代码块)、懒汉式(线程不安全)、懒汉式(线程安全,同步方法)、懒汉式(线程不安全,同步代码块)、双重检查(推荐,线程安全、懒加载)、静态内部类(推荐)、枚举(推荐)
设计模式——设计模式介绍和单例设计模式
|
1月前
|
调度
【多线程-从零开始-陆】wait、notify和notifyAll
【多线程-从零开始-陆】wait、notify和notifyAll
23 0
|
3月前
|
设计模式 JavaScript 前端开发
从工厂到单例再到策略:Vue.js高效应用JavaScript设计模式
【8月更文挑战第30天】在现代Web开发中,结合使用JavaScript设计模式与框架如Vue.js能显著提升代码质量和项目的可维护性。本文探讨了常见JavaScript设计模式及其在Vue.js中的应用。通过具体示例介绍了工厂模式、单例模式和策略模式的应用场景及其实现方法。例如,工厂模式通过`NavFactory`根据用户角色动态创建不同的导航栏组件;单例模式则通过全局事件总线`eventBus`实现跨组件通信;策略模式用于处理不同的表单验证规则。这些设计模式的应用不仅提高了代码的复用性和灵活性,还增强了Vue应用的整体质量。
50 1