从0开始深入理解并发、线程与等待通知机制(下)

线程间的通信与协调,协作

        synchronized内置锁

        Java 支持多个线程同时访问一个对象或者对象的成员变量,但是多个线程同时访问同一个变量,会导致不可预料的结果。关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,使多个线程访问同一个变量的结果正确,它又称为内置锁机制

        对象锁和类锁 

        对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的 class 对象上的。

从0开始深入理解并发、线程与等待通知机制(下)_第1张图片

        如图所示,这个就是类锁。

        我们知道,类的对象实例可以有很多个,所以当对同一个变量操作时,用来做锁的对象必须是同一个,否则加锁毫无作用。比如下面的示例代码:

        从0开始深入理解并发、线程与等待通知机制(下)_第2张图片

        但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的 class 对象,但是每个类只有一个 class 对象,所以每个类只有一个类锁。

       volatile,最轻量的通信/同步机制

        volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。同时不是锁的机制,不能保证线程安全。

从0开始深入理解并发、线程与等待通知机制(下)_第3张图片

        不加 volatile 时,子线程无法感知主线程修改了 ready 的值,从而不会退出循环,而加了 volatile 后,子线程可以感知主线程修改了 ready 的值,迅速退出循环。但是 volatile 不能保证数据在多个线程下同时写时的线程安全, volatile 最适用的
场景:一个线程写,多个线程读。

        等待/通知机制

        线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”(what)和“怎么做”(How),简单的办法是让消费者线程不断地循环检查变量是否符合预期在 while 循环中设置不满足的条件,如果条件满足则退出 while 循环,从而完成消费者的工作。却存在如下问题:

        1)难以确保及时性。

        2)难以降低开销。如果降低睡眠的时间,比如休眠 1 毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。

        等待/通知机制则可以很好的避免,这种机制是指一个线程 A 调用了对象 O的 wait()方法进入等待状态,而另一个线程 B 调用了对象 O notify()或者 notifyAll()方法,线程 A 收到通知后从对象 O wait()方法返回,进而执行后续操作。上述两个线程通过对象 O 来完成交互,而对象上的 wait()notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

        notify():
                通知一个在对象上等待的线程,使其从 wait 方法返回 , 而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入 WAITING 状态。
        notifyAll():
                通知所有等待在该对象上的线程
        wait()
                调用该方法的线程进入 WAITING 状态 , 只有等待另外线程的通知或被中断才会返回. 需要注意 , 调用 wait() 方法后 , 会释放对象的锁
        wait(long)
                超时等待一段时间,这里的参数时间是毫秒 , 也就是等待长达 n 毫秒 , 如果没有通知就超时返回
        wait (long,int)
                对于超时时间更细粒度的控制,可以达到纳秒

        等待和通知的标准范式

        等待方遵循如下原则。

                1)获取对象的锁。
                2)如果条件不满足,那么调用对象的 wait() 方法,被通知后仍要检查条件。
                3)条件满足则执行对应的逻辑。
         从0开始深入理解并发、线程与等待通知机制(下)_第4张图片
        通知方遵循如下原则。
                1)获得对象的锁。
                2)改变条件。
                3)通知所有等待在对象上的线程。
        在调用 wait ()、 notify() 系列方法之前,线程必须要获得该对象的对象级 别锁,即只能在同步方法或同步块中调用 wait ()方法、 notify() 系列方法 ,进入 wait ()方法后,当前线程释放锁,在从 wait ()返回前,线程与其他线程竞争重新获得锁,执行 notify() 系列方法的线程退出调用了 notifyAll synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

        notify notifyAll 应该用谁

        尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程

        模拟快递等待通知机制

        快递实体

package com.laoyang.Thread.快递模拟通知;

/**
 * @author:Kevin
 * @create: 2023-10-07 15:06
 * @Description: 快递实体类i
 */

public class Express {
    public final static String DIST_CITY = "ShangHai";
    public final static int TOTAL = 500;
    private int km ;/*快递运输里程数*/
    private String site;/*快递到达地点*/

    public Express() {
    }

    public Express(int km, String site) {
        this.km = km;
        this.site = site;
    }

    public void change(){
        if (km < TOTAL){
            km = km +100;
            System.out.println("the Km is "+this.km);
        }
        if(km >= TOTAL){
            site = DIST_CITY;
            System.out.println("the Express is arrived");
        }
    }

    /*线程等待公里的变化*/
    public synchronized void waitKm(){
        while(this.km <= TOTAL){
            try {
                wait();
                System.out.println("Map thread["
                        +Thread.currentThread().getId()
                        +"] wake,I will change db");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /*线程等待目的地的变化*/
    public synchronized void waitSite(){
        while(!this.site.equals(DIST_CITY)){
            try {
                wait();
                System.out.println("Notice User thread["+Thread.currentThread().getId()
                        +"] wake");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("the site is "+this.site+",I will call user");
    }
}

        测试通知

package com.laoyang.Thread.快递模拟通知;

/**
 * @author:Kevin
 * @create: 2023-10-07 15:07
 * @Description: test
 */

public class TestWN {
    private static Express express = new Express(0,"WUHAN");

    /*检查里程数变化的线程,不满足条件,线程一直等待*/
    private static class CheckKm extends Thread{
        @Override
        public void run() {
            express.waitKm();
        }
    }

    /*检查地点变化的线程,不满足条件,线程一直等待*/
    private static class CheckSite extends Thread{
        @Override
        public void run() {
            express.waitSite();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<2;i++){
            new CheckSite().start();
        }
        for(int i=0;i<2;i++){
            new CheckKm().start();
        }

        Thread.sleep(500);
        for(int i=0; i<5; i++){
            synchronized (express){
                express.change();
                express.notifyAll();
            }
            Thread.sleep(500);
        }
    }

}

        面试题

        方法和锁

        调用 yield() sleep()wait()notify()等方法对锁有何影响?

        前两者属于线程,后两者属于对象

        yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。

        调用 wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行 wait 方法后面的代码。

        调用 notify()系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才会自然而然的释放锁,所以 notify()系列方法一般都是 syn 同步代码的最后一行。

        wait notify 

        为什么 wait notify 方法要在同步块中调用?
        原因

        主要是因为 Java API 强制要求这样做,如果你不这么做,你的代码会抛出 IllegalMonitorStateException 异常。其实真实原因是:

        这个问题并不是说只在 Java 语言中会出现,而是会在所有的多线程环境下出现。

        假如我们有两个线程,一个消费者线程,一个生产者线程。生产者线程的任 务可以简化成将 count 加一,而后唤醒消费者;消费者则是将 count 减一,而后在减到 0 的时候陷入睡眠:

        生产者伪代码:
        count+1;
        notify();
        消费者伪代码:
        while(count<=0)
        wait()
        count--
        万一这些步骤混杂在一起呢?比如说,初始的时候 count 等于 0 ,这个时候消费者检查count 的值,发现 count 小于等于 0 的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了, 也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了……

从0开始深入理解并发、线程与等待通知机制(下)_第5张图片

        这就是所谓的 lost wake up 问题。
        那么怎么解决这个问题呢?

        现在我们应该就能够看到,问题的根源在于,消费者在检查 count 到调用wait()之间,count 就可能被改掉了。

        这就是一种很常见的竞态条件。
        很自然的想法是,让消费者和生产者竞争一把锁,竞争到了的,才能够修改count 的值。

你可能感兴趣的:(并发编程,java,开发语言,并发)