【多线程-从零开始-陆】wait、notify和notifyAll

简介: 【多线程-从零开始-陆】wait、notify和notifyAll

线程饿死

一个或多个线程因为无法获得执行所需的资源(如CPU时间、锁、或其他同步控制)而被长时间阻塞或延迟执行的情况。尽管这些线程可能处于可执行状态并且已经准备好运行,但由于资源分配的不均衡或调度策略的问题,它们无法获得执行的机会。

例子

你去 ATM 机取钱,在你后面还排了很对人。你进去后,门锁上了,但你发现 ATM 机里面没钱了,于是你出去了。

按理来说,你出来之后,应该轮到排在你后面的人进去了,可你刚把锁打开,前脚刚迈出去,却心想“里面会不会又有钱了?”,于是你又进去了,把门又锁上了,可你进去之后发现还是没钱。

于是你就这样反反复复、进进出出、开锁上锁。虽然你的行为没有造成任何死锁,但你后面的人却做不了任何事(其他线程无法执行任何逻辑),这就叫“线程饿死

  • 这属于一个概率性问题,和调度器具体的策略直接相关
  • 针对上述问题,同样可以使用 wait / notify 来解决
  • 让你在拿到锁的时候进行判定,判定当前是否执行“取钱”操作,如果能执行,就正常执行;如果不能执行,就需要主动释放锁,并且“阻塞等待”(通过调用 wait),此时这个线程就不会在后续参与锁的竞争了
  • 一直阻塞到“取钱”的条件具备了,此时再由其他线程通过通知机制(notify)唤醒这个线程

wait

因为线程在操作系统上的调度是随机的,而我们不喜欢随机,喜欢确定,所以增加了一些手段来让调度变得确定

  • 多个线程,需要控制线程之间执行某个逻辑的先后顺序,就可以让后执行的逻辑使用 wait,先执行的线程完成某些逻辑后,通过 notify 唤醒对应的 wait
  • 另外通过 waitnotify 也是为了解决“线程饿死”问题

当我们尝试使用 wait:

public class Demo3 {  
    public static void main(String[] args) throws InterruptedException {  
        Object obj = new Object();  
        System.out.println("wait 之前");  
        obj.wait();  
        System.out.println("wait 之后");  
    }
}

运行后,报错了

  • Monitor:此处指的是 synchronized 这里的锁
  • 合起来为:非法的锁状态异常(加锁状态/未加锁状态)

  • 之所以会出现这种情况,是因为 wait 中会进行一个操作,就是针对 obj 对象,先进行解锁。所以,使用 wait 的时候,务必要放到 synchronized 代码块里面(必须得先加上锁,才能谈“解锁”)
  • 你进入 ATM 机后,发现没钱,就要“阻塞等待”,此时一定是你先解锁,再开门出去。就是先释放锁,再等待。如果你抱着锁等待,就也没把几回让给别人,因为别人也无法进去

  • 并且释放锁和加上锁这两个操作是通过 wait 同时来进行(打包成原子),若不是同时执行,那就可能发生线程切换
  • 比如在释放锁之后,插进来了一个通知正在等待代的线程继续执行操作的线程,可是前面那个才刚刚释放锁,还没开始进行执行等待的操作,最终这个线程由于错过了通知,将持续等待下去

public class Demo3 {  
    public static void main(String[] args) throws InterruptedException {  
        Object obj = new Object();  
        System.out.println("wait 之前");  
        synchronized(obj){  
            obj.wait();  
        }        
        System.out.println("wait 之后");  
    }
}
//打印结果为:wait 之前
  • 因为代码阻塞在中间了,所以后面的逻辑就无法完成,此时线程的状态变成了 WAITING(没有超时时间的等待,有超时时间的事 TIMED_WAITING
  • 并且由于代码中没有 notify,所以 wait 将一直持续等待下去

综上

  • wait 使调用的线程进入阻塞
  • wait做三件事
  1. 释放锁
  2. 进入阻塞状态,准备接受通知
  3. 收到通知后,唤醒,并且重新尝试获取锁

注意

  • wait 默认是“死等”,但它还提供了一个带参数的版本,指定超时时间。若 wait 达到了最大时间,还没等到 notify,就不会继续等待了,而是继续执行
  • wait(1000)sleep(1000)还是有本质区别的
  • 使用 wait 的本质目的是为了提前唤醒,而 sleep 就是固定时间的阻塞,不涉及唤醒(虽然 sleep 可以被 Interrupt 唤醒,但这是一个终止线程的操作,而不是唤醒)
  • wait 必须要搭配 synchronized 使用,并且 wait 会先释放锁,同时进行等待
  • sleep 和锁无关,如果不加锁,sleep 可以正常使用;如果加了锁,sleep 操作不会释放锁,会“抱着锁”,一起睡,其他线程无法拿到锁

notify唤醒wait的操作

public class Demo5 {  
    private static Object locker = new Object();  
  
    public static void main(String[] args) {  
        Thread t1 = new Thread(() -> {  
            synchronized (locker){  
                System.out.println("t1 wait之前");  
                try {  
                    locker.wait();  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }                System.out.println("t1 wait之后");  
            }        
        });        
        Thread t2 = new Thread(() -> {  
            System.out.println("t2 notify之前");  
            Scanner scanner = new Scanner(System.in);  
            scanner.next(); //此处是通过这个next构造一个“阻塞”的状态  
  
            synchronized (locker) {  
                locker.notify();  
            }            
            System.out.println("t2 notify之后");  
        });        
        t1.start();  
        t2.start();  
    }
}
//运行结果:
t1 wait之前
t2 notify之前
9
t2 notify之后
t1 wait之后
  • 要保证加锁的对象和调用 wait 的对象是一样的,如果不是同一个对象,那么无法使用,因为锁的状态是不对的
  • 要确保调用 waitnotify 的对象是一样的才能唤醒
  • waitnotify调用前都要加上锁
  • 因为在多线程中,一个线程加锁,一个不加,是无意义的,不会有任何的阻塞效果。此处希望 t2 执行 notify 的时候,t1 是未持有锁的状态,如果 t1 正持有锁,肯定就不是在 wait,这个时候去 notify 也没有意义。所以要确保 notify 拿到锁,再去 notify 唤醒 wait
  • 一定要确保持有锁才能谈释放
  • 假设是多个现场,如果多个线程都在同一个对象上 waitnotify 只会随机唤醒其中一个

notifyAll

notify 相对,还有一个 notifyAll

将上面的notify换成notifyAll之后
运行结果为:
---
t1 wait之前
t3 wait之前
t2 wait之前
9
t4 notifyAll之前
t4 notifyAll之后
t1 wait之后
t2 wait之后
t3 wait之后
  • 大部分情况下,都是使用 notify。若要唤醒多个,就一个一个地唤醒,整个程序执行过程是比较有序的,如果一下全唤醒,这些被唤醒的线程就会无序的竞争锁(会很混乱,可能带来未知的风险)
  • notifynotifyAll 通知的时候,如果没有线程在 wait,不会有任何副作用

练习

创建三个线程,使用 waitnotify 控制先后打印 A、B、C 三个字母

public class Demo6 {  
    private static Object locker = new Object();  
    private static Object locker2 = new Object();  
  
    public static void main(String[] args) {  
        Thread t1 = new Thread(() -> {  
            System.out.println("A");  
            //Thread.sleep(1000);
            synchronized (locker) {  
                locker.notify();  //这个notify用来唤醒t2中的wait,因为是locker锁
            }        
        });        
        Thread t2 = new Thread(() -> {  
            synchronized (locker) {  
                try {  
                    locker.wait();  //被t1的notify唤醒
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }                
                System.out.println("B");  
  
                synchronized (locker2){  
                    locker2.notify();  //这个notify用来唤醒t3中的wait,因为是locker2锁
                }            
            }        
        });        
        Thread t3 = new Thread(() -> {  
            synchronized (locker2) {  
                try {  
                    locker2.wait();  //被t2的notify唤醒
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }            
            System.out.println("C");  
        });        
        t1.start();  
        t2.start();  
        t3.start();  
    }
}
  • 这个代码中,若是先执行 t2wait,后执行 t1notify,代码逻辑就一切顺利(大概率就是这样,因为 t1 的打印需要不少时间)
  • 但存在这样的可能:t1 限制性了打印和 notify,然后 t2 才执行 wait,意味着通知来早了,t2 错过了通知,t2wait 就无人唤醒了
  • 为了解决这样的情况,我们只需要在 t1 里面加一个 sleep 就可以了 (只要在锁的前面就行),让 t1 线程等一会,其他线程先执行


相关文章
|
15天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
12 1
|
15天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
22 1
|
15天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
26 1
|
30天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
38 1
C++ 多线程之初识多线程
|
15天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
13 3
|
15天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
12 2
|
15天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
27 2
|
15天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
26 1
|
15天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
21 1
|
30天前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
43 6