多线程(初阶五:wait和notify)

简介: 多线程(初阶五:wait和notify)



一、概念

我们知道,多线程在系统中的调度是随机的,我们不能干预多个线程的执行顺序,但是我们可以使某个线程放弃被系统调用,让其他线程先被调用,这样,可以达到我们的预期效果;

wait就是让多线程进行锁竞争的时候,让后执行的线程,放弃和别的线程进行锁竞争,别的线程执行完后,别的线程使用notify,将wait的线程不想进行锁竞争这个信息释放掉,再次和其他线程锁竞争。等待,通知的机制(和join用途类似)


二、用法

(1)举个栗子:

现在有很多滑稽老铁要去ATM里,滑稽A是取钱的,滑稽B是存钱的,滑稽C是运钞票的人员,负责给ATM机补充钱,防止ATM机没钱了,别人取不到钱。而这里的滑稽A,滑稽B,滑稽C我们当做是线程,每次去ATM机里,只能有一个人进去,相当于上锁了,其他人不能进去,等ATM机里面的人完成操作后,出来后,别的人才能进去,但是这里是多线程的原因,其他线程会有锁竞争。    

当A进去ATM机里后,就上锁,其他人不能进去,如图:

把滑稽A比作是线程,当A线程进去后就会上锁,A线程要进行取钱的操作,其他线程不能进去操作,当A线程执行完自己的操作后,其他线程才能去锁竞争。

但是如果ATM机里面没有钱时,A线程就不能完成取钱这个操作,它会退出ATM机,但退出后呢,它因为没有完成取钱的操作,就会想继续进去ATM里面,完成取钱这个操作,就会继续和其他线程进行锁竞争,因为A线程拿到了锁,处于RUNNABLE状态,其他线程因为阻塞,处于BLOCKED状态,需要被系统唤醒后,才能去竞争锁,但是线程A呢,不用唤醒就能去竞争锁,后面又被A线程拿到锁的可能性还是很大的(类似近水楼台先得月)。如果这样子,那线程A频繁的进去又出来,干不了事,但是其他线程也不能进去操作,用通俗的话说,就是占着茅坑不拉屎的意思。也就出现线程安全问题了。其他线程,无法拿到锁,这个情况称为 “线程饿死”。

这里的线程A的代码大概逻辑是这样的:

当A线程没有取到钱,就会一直重复加锁,解锁的操作。

这样的bug没有死锁那么严重,但也是要解决的。那如何解决呢。这时,就可以用wait和notify了。期望改进成,如下图:

这里的wait内部做了三件事

(1)释放锁,给其他线程竞争锁

(2)进入阻塞等待

(3)等其他线程使用notify后,解除wait,参与到锁竞争中

(2)wait和notify的使用

wait的使用前提必须是当前对象被上锁了才能使用,不能你对象没被上锁,就wait了,那也不知道是在wait谁。同时,有线程wait了,也必须有其他线程notify来释放这个wait,不然这个wait就会一直阻塞。

1、没有上锁的wait

代码:

public class TestDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        locker.wait();
    }
}

执行结果:

2、当一个线程被wait,但没有其他线程notify来释放这个wait

代码:

public class TestDemo3 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("wait之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("wait之后");
            }
        });
        t1.start();
    }
}

执行结果:

打印不了 “wait之后”,一直是阻塞等待状态,在jconsole中,状态如图

3、两个线程,有一个线程wait,有一个线程notify来释放wait

代码:

public class TestDemo2 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1 wait之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2 wait之后");
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker) {
                System.out.println("t2 notify之前");
                locker.notify();
                System.out.println("t2 notify之后");
            }
        });
        t1.start();
        t2.start();
    }
}

注意:这里的释放wait的notify,用的对象必须是要一样的,如果不一样,wait是不能被释放的,t1也就不能被唤醒了.

在系统中,notify可以不用上锁,但是在java中,规定了要上锁,而且上锁的对象也要和notify对象一样,所以和系统是有区别的。

执行结果:

结果解析:

t1 和 t2 执行的时候

(1)因为t1 sleep了1秒,所以能保证t1 先wait,所以先打印 “t1 wait之前”,这时,t1就进入阻塞等待状态。

(2)t2线程sleep了1秒后,获得这个locker锁,打印“t2 notify 之前”,当t2线程执行了notify后,t1 线程的wait就被释放了。

(3)因为t2还在持有锁,所以t1会还会进入阻塞,t2打印 “t2 notify之后” ,释放锁。

(4)t1拿到锁,再打印“t1 wait之后”。

4、notifyAll

唤醒等待这个对象的所有线程;假设有很多个线程,都使用同一个对象wait,这时,使用notifyAll,所有使用了这个对象的wait的线程,都会被唤醒。

注意:当这些线程都被唤醒时,就要重新获取锁,他们还是要进行锁竞争的,这里也就相当于串行执行了(线程调度还是随机调度的)。而且使用notifyAll后,全部使用同一对象wait的线程,都被唤醒了,不好控制,更加推荐使用notify。

(3)wait的三个选项

如图:

没有参数的就是死等,但是很多情况,死等是不合理的,所以我们加参数,就是让某个线程在一定时间wait,如果超出了这个时间,就不wait了,直接过掉wait。

有一个参数的精确范围是毫秒级别,两个参数的精确范围是纳秒级别。


三、wait、sleep、join

wait:需要搭配synchronized使用,线程wait时,处于WAITING状态,需要其他线程notify后,才能被唤醒,或者设置时间,到时就唤醒,可以兜底。

sleep:线程sleep时,要到一定休眠时间才能被唤醒,但是也能被interrupt终止,但是这种情况是会抛异常的,是非常规手段,不符合我们预期的效果。

join:啥线程调用join,当前线程就要等啥线程执行完,才能之前当前线程;和wait一样有参数可以选择,到时就不等了。

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