【JavaEE】wait 和 notify

哈喽,大家好~我是保护小周ღ,本期为大家带来的是 Java 线程的有序调度,由于线程之间的调度是无序的,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序,这个时候就可以使用 wait() 方法和 notify() 方法来控制,具体方法、具体实现,确定不来看看嘛~
更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★* ‘

一、线程有序执行

线程是系统调度的基本单位,但是线程之间的是抢占式执行,调度无序,所以多线程编程就容易引发线程安全问题,上期博主详细讲述了线程安全问题感兴趣的朋友可以去看看,有时候我们希望线程的调度循序

可以按照我们设计的顺序执行,所以这就需要使用到今天介绍的三个方法——wait 和 notify / notifyAll

以上方法都属于 Object 类的方法,只要是类对象(不包含基本数据类型)都可以使用。

wait() 发现当前线程的执行条件不成熟就先进入等待状态 (WAITING),notify() 另一个线程构造了一个成熟的条件就可以唤醒阻塞的线程。

举个例字:

所谓阻塞等待:线程暂时不参与系统的调度,也就参与CPU 的执行。

张三去银行取钱(线程),取号等待,然后被呼叫了(线程调度执行),1 号柜台小姐姐(CPU 核心)服务张三老铁,为他处理业务,线程运行状态,张三提出请求,我要取5 万元,1 号柜台小姐姐进行一顿操作之后对张三说:“不好意思,银行现在余额不足,现在无法为您办理业务,请稍作等待~”。张三(wait() )进入等待状态,张三等待的时间里, 李四来银行存钱,同样是由 1 号柜台小姐姐处理业务,存10 万, 李四可以看作是另一个线程,柜台小姐姐为李四处理业务后,李四临走时告诉张三:“老铁我刚刚存了10 万块,你可以去取钱了”(notify() ), 柜台小姐姐此时也通知张三老铁重新取号排队,张三就从等待状态进入了就绪状态,随时准备被CPU 调度执行。
以上事例:当张三线程发现取5 万元这个操作无法继续进行时(银行钱不够)就 (wait() )进入阻塞等待状态,李四线程刚好去银行存了10 万元,使得张三老铁的取钱操作满足了条件,并且还提醒了“张三老铁可以去取钱了(notify() )”,于是张三才从阻塞等待状态中解除。这个提醒非常的关键,李四老铁即使存了钱,如果不告诉 张三(notify())那么张三就会一直等待下去。

1.1 wait() 方法

线程调用 wait() 方法主要有会做以下三种事:

  1. 解除对象锁状态,其他线程就可以对当前对象进行操作了(synchronized 修饰的代码块)

  1. 调用线程会进入阻塞等待状态

  1. 当收到结束等待的通知后(notify())就唤醒等待线程(接受CPU 的调度执行),并且尝试重新获取锁,synchronized 修饰的代码块,需要获取到锁才能继续执行,否则阻塞等待其他线程释放锁。这涉及到多线的执行相同对象的安全问题

所以我们在使用 wait() 方法的大前提是必须在 synchaonized(加锁) 的状态下,如果在没有加锁的代码块中使用 wait() 方法就会抛出 “IllegalMonitorStateException”异常。

大家可能有疑问就是为什么 wait() 方法要建立在 synchronized 修饰的代码块的基础上,notify 和 wait 的功能需要保证可见性的基础上才能满足,比如一个线程发送了notify ,另一个线程的wait需要感知到, 两个线程之间通讯是存在静态条件,重新加锁的过程会使得线程重新从内存中读取数据,保障了 wait() 线程的数据的有效性。


wait() 结束等待的条件:

  1. 其他线程调用使用wait () 方法的对象的 notify() 方法 (都来自Object 类)

  1. wait() 有个带参数的版本可以设置最大等待时间,来指定线程的等待的时间

  1. 其他线程调用该等待线程的 interrupted() 方法(判断线程是否设置了中断标志位,这是Thread 中断线程的一个机制),导致抛出InterruptedException 异常

wait() 方法的使用:

上图可见,由于 t1 线程设置了 wait() 方法,所以线程进入了 等待状态,没有打印t1线程结束。

如此,我们只有使用上述三种方式来结束 wait 状态。大多数情况下使用 notify() 方法,也可以使用 wait() 带参数的版本设置一个最大等待时间。

this.wait(1000) // 使当前线程进入等待状态,一秒后唤醒

1.2 notify() 方法

notify() 方法的作用就是用来唤醒等待的线程的。

该方法的作用:

  1. 保证该方法是在可执行的线程上,同时也要放在 synchronized 修饰的代码块中,该方法是用来通知那些可能等待该对象的对象锁的其他线程发出通知,使得这些线程重新尝试获取该对象的对象锁(可能会发生锁竞争)。

  1. 在调用 notify() 方法后,当前线程不会马上释放该对象锁,要等调用notify() 方法的线程将 synchronized 修饰的代码块中的程序执行完,才会释放对象锁。

  1. 如果有多个线程因为 wait() 进入等待状态, notify() 只会在这些线程中随机挑选出一个进行唤醒(没有先来后到之分)。

使用 notify() 方法 解除 wait() 线程

public static void main(String[] args) throws InterruptedException {
        //使用第三方对象锁,每个对象都自带了一把锁
        Object locker = new Object(); 
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t1 线程执行");
                // 加锁后,别的线程无法获取到 locker 对象锁就会进入阻塞等待
                synchronized (locker) { 
                    try {
                        // 对当前对象设置了 wait() 线程进入阻塞等待,并释放 locker 对象锁,别的线程就可以使用锁了
                        locker.wait(); 
                        System.out.println("t1 线程结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t2 线程执行");
                // 加锁后,别的线程无法获取到 locker 对象锁就会进入阻塞等待
                synchronized (locker) { 
                    try {
                        // 对当前对象设置了 wait() 线程进入阻塞等待,并释放 locker 对象锁
                        locker.wait(); 
                        System.out.println("t2 线程结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        t1.start();// 启动线程
        t2.start();

        // 因为线程是并发执行所以,main 休眠等待 t1 线程进入 wait() 状态
        Thread.sleep(1000); 

        //主线程main 执行
        synchronized (locker) {
            //随机唤醒一个wait() 线程
            locker.notify(); 
        }
    }

由此可见, 有两个线程进入了 wait() 状态,但是只调用了一次 notify() 所以只能随机解除一个 wait() 线程,此时 t2 线程还在等待中,如果我们调用两次 notify() 方法那就会唤醒两个线程。


1.3 notifyAll() 方法

我们在程序设计中难免会遇到线程使用一个对象锁的情况,那么我们可以使用 notifAll() 对这些使用同一个对象锁的线程并且是处于 wait() (线程等待)状态,随机唤醒一个线程,其他线程任然是(waiting 等待状态)。

此时我们使用 notifyAll() 方法就可以唤醒所有等待的线程。

虽然 notifyAll() 方法可以同时唤醒 多个线程,但是对象锁只有一个,此时这些个线程就会发生锁竞争,那个线程先抢到锁就可以 CPU调度执行,其他线程其实是进入了阻塞等待的状态,等待该线程释放锁然后其他继续竞争锁。


1.4 线程有序调度

说起有序调度当然还得是使用我们的wait 和 notify 方法,多个线程使用一个锁对象,如果有多个线程因为 wait() 进入等待状态, notify() 只会在这些线程中随机挑选出一个进行唤醒(没有先来后到之分)。

这样就达不到我们想要的效果哦,所以我们可以尝试使用不同的对象锁根据我们的设计想法来对线程进行加锁解锁,欲知如何实现,请听下文讲述。

例题: 有三个线程 A,B ,C , A线程打印 A , B 线程打印B ,C 线程打印C , 三个线程同时启动,循环10次按照顺序打印 : A, B , C

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        // 创建三个锁对象B
        Object locker1 = new Object();// 线程 A 使用
        Object locker2 = new Object();// 线程 B 使用
        Object locker3 = new Object();// 线程 C 使用

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (locker1) { //获取locker1 锁对象
                    try {
                        locker1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print("A");
                synchronized (locker2) {
                    locker2.notify(); // 线程 A 给 线程 B 解锁
                }
            }
        },"A");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (locker2) { //获取locker2 锁对象
                    try {
                        locker2.wait(); // 线程 B 进入阻塞状态
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print("B");
                synchronized (locker3) {
                    locker3.notify(); // 线程 B 给 线程 C 解锁
                }
            }
        },"B");

        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (locker3) { //获取locker3 锁对象
                    try {
                        locker3.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("C");
                synchronized (locker1) {
                    locker1.notify(); // 线程 C 给 线程 A 解锁
                }
            }
        },"C");

        //同时启动
        t1.start();
        t2.start();
        t3.start();
        // 主线程等待 线程 ABC 进入等待 wait() 等待状态
        Thread.sleep(1000);
        synchronized (locker1) { //给线程 A解锁,先从A开始
            locker1.notify();
        }
    }
}

如此,我们就可以利用 wait() 和 notify() 灵活的控制线程的执行顺序,线程之间相互制约。


1.5 wait() 和 join() 的区别

wait() 和 join() 都可以让一个线程等待另一个线程先执行,

public class Demo6 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            try { 
                Thread.sleep(1000); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (locker) { // t1 线程给 t3 线程解锁
               locker.notify();
               System.out.println("t1 线程执行");
           }
        });

        Thread t2= new Thread(() -> {
            System.out.println("t2 线程执行");
        });

        Thread t3 = new Thread(() ->{
            try {
                t2.join(); // t3 等待t2 线程执行完毕

                synchronized (locker) { // 进入 wait() 等待状态
                    locker.wait();
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t3 线程执行");
        }) ;

        t1.start();
        t2.start();
        t3.start();
    }
}

join 也算是一种控制线程顺序的方式, 但是本质上的区别在于,join 是一个线程等待另一个线程执行完毕后再继续执行,wait() 就是让线程停下来等一等,另一个线程调用notify 就可以把该线程唤醒就能够继续执行,多线程之间针对某一个代码块的执行顺序就很灵活。


1.6 wait() 和 sleep() 的区别

wait() 有个带参数的版本,Time_Waiting 超时等待状态:设置了最大等待时间,超出最大等待时间就换醒线程,sleep() 带参数同样也可以这样设置(单位是毫秒 1000 = 1 秒)。

两个方法(带参数)都可以使得线程进入 Time_Waiting 超时等待状态。

  1. wait() 是 Object 类的方法,sleep() 是 Thread 类的方法

  1. wait() 必须在synchronized修饰的代码块或方法中使用,sleep方法可以在任何位置使用;

  1. wait() 被调用后当前线程进入BLOCK状态并释放锁,并且可以通过notify和notifyAll方法进行唤醒;sleep() 被调用后当前线程进入TIMED_WAIT状态,不涉及锁相关的操作。


到这里,Java 多线程有序调度的方法,博主已经分享完了,希望对大家有所帮助,如有不妥之处欢迎批评指正。

本期收录于博主的专栏——JavaEE,适用于编程初学者,感兴趣的朋友们可以订阅,查看其它“JavaEE基础知识”。

下期预告:Java常用模式——单例模式

感谢每一个观看本篇文章的朋友,更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★* ‘

猜你喜欢

转载自blog.csdn.net/weixin_67603503/article/details/129790676