多线程(五):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关键字,禁止指令重排序,保证后面的线程拿到的是完整的对象

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

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


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

相关文章
|
2月前
|
Java 程序员
从菜鸟到大神:JAVA多线程通信的wait()、notify()、notifyAll()之旅
【6月更文挑战第21天】Java多线程核心在于wait(), notify(), notifyAll(),它们用于线程间通信与同步,确保数据一致性。wait()让线程释放锁并等待,notify()唤醒一个等待线程,notifyAll()唤醒所有线程。这些方法在解决生产者-消费者问题等场景中扮演关键角色,是程序员从新手到专家进阶的必经之路。通过学习和实践,每个程序员都能在多线程编程的挑战中成长。
36 6
|
2月前
|
安全 Java
JAVA多线程通信新解:wait()、notify()、notifyAll()的实用技巧
【6月更文挑战第20天】Java多线程中,`wait()`, `notify()`和`notifyAll()`用于线程通信。在生产者-消费者模型示例中,它们确保线程同步。`synchronized`保证安全,`wait()`在循环内防止虚假唤醒,`notifyAll()`避免唤醒单一线程问题。关键技巧包括:循环内调用`wait()`,优先使用`notifyAll()`以保证可靠性,以及确保线程安全和正确处理`InterruptedException`。
32 0
|
2月前
|
安全 Java
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
【6月更文挑战第20天】JAVA多线程中,wait(), notify(), notifyAll()是Object类的关键同步机制。wait()让线程等待并释放锁,直到被notify()或notifyAll()唤醒或超时。它们必须在同步块中使用,持有锁的线程调用。notify()唤醒一个等待线程,notifyAll()唤醒所有。最佳实践包括:与synchronized结合,循环检查条件,避免循环内notify(),通常优先使用notifyAll()。
23 0
|
5天前
|
设计模式 Java
【Java】单例设计模式
【Java】单例设计模式
|
18天前
|
设计模式 SQL 安全
单例模式大全:细说七种线程安全的Java单例实现,及数种打破单例的手段!
设计模式,这是编程中的灵魂,用好不同的设计模式,能使你的代码更优雅/健壮、维护性更强、灵活性更高,而众多设计模式中最出名、最广为人知的就是Singleton Pattern单例模式。通过单例模式,我们就可以避免由于多个实例的创建和销毁带来的额外开销,本文就来一起聊聊单例模式。
|
1月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
54 1
|
1月前
|
设计模式 存储 安全
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
35 1
|
2月前
|
设计模式 Java 编译器
设计模式——创建型模式(工厂,简单工厂,单例,建造者,原型)
设计模式——创建型模式(工厂,简单工厂,单例,建造者,原型)
|
1月前
|
设计模式 存储 缓存
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
36 0
|
1月前
|
设计模式 缓存 安全
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
21 0