【并发编程】 --- 线程间的通信wait、notify、notifyAll

源码地址:https://github.com/nieandsun/concurrent-study.git


1 wait、notify、notifyAll简单介绍


1.1 使用方法 + 为什么不是Thread类的方法

为什么不是Thread类的方法

首先应该明确wait、notify、notifyAll三个方法都是对锁对象的操作,而锁可以是任何对象。在java的世界中任何对象都属于Object类,因此这三个方法都是Object的方法, 而不是线程对象Thread的方法。

使用方法

需要注意两点:

  • (1)这三个方法必须在synchronized关键字包含的临界区(简单理解,就是代码块)内使用
  • (2)使用方式为锁对象.方法(),比如obj.wait();

1.2 什么时候加锁、什么时候释放锁?

必须要明确以下几点:

  • (1)notify和notifyAll方法不会释放锁,这两个方法只是通知其他使用该锁当锁但是在wait状态的线程,可以准备抢锁了
    • 这里还要格外注意一点,其他使用该锁当锁且处于wait状态的线程只有被notify或notifyAll唤醒了,才有资格抢锁
  • (2)某个锁对象调用wait方法会立即释放当前线程的该对象锁 , 且其他线程通过notify/notifyAll方法通知该线程可以抢该对象锁时,如果当前线程抢到了,会从当前锁的wait方法之后开始执行 — 即从哪里wait,从哪里执行;
  • (3)在synchronized、wait、notify、notifyAll的组合里
    • 加锁的方式只有一个即进入同步代码块时加锁;
    • 释放锁的方式有两个: ①锁对象调用wait方法时会释放锁 ;② 走完同步代码块时自动释放锁

1.3 notify、notifyAll的区别

  • 某个锁对象的notify只会唤醒一个使用该锁当锁且处于wait状态的线程;
  • 某个锁对象的notifyAll方法会把所有使用该锁当锁且处于wait状态的线程都唤醒;

使用建议: 为了防止某些线程无法被通知到,建议都使用notifyAll。


2 两个比较经典的使用案例

感觉上学的时候好像就考过下面这两个案例☺☺☺


2.1 案例1 — ABCABC。。。三个线程顺序打印问题


2.1.1 题目

三个线程,线程A不停打印A、线程B不停的打印B、线程C不停的打印C,如何通过synchronized、wait、notifyAll(或notify)的组合,使三个线程不停地且顺序地打印出ABCABC。。。


2.1.2 题目分析

其实我在《【并发编程】— Thread类中的join方法》这篇文章里用join实现过类似的功能,有兴趣的可以看一下。。。

如果使用synchronized、wait、notifyAll(或notify)的组合的话,这个问题可以归结为下图所示的问题。即:

线程A走完 ,线程B走 —> 线程B走完,线程C走 —》 线程C走完,线程A走 。。。。

在这里插入图片描述
以线程A为起点进行分析,可知:

  • (1)要想线程A走完,线程B接着走,那肯定是线程A释放了线程B所需要的锁,这里设该锁为U,做进一步分析可知:

    • 既然线程B需要线程A释放的锁U,那就意味着此时线程B中的锁U肯定处于wait状态;
    • 同时要想线程A释放了锁U之后,线程B可以被唤醒,线程A还必须得进行锁U的notify或notifyAll
  • (2)同理,要想线程B走完,线程C走,那肯定是线程C有一把处于wait状态的锁,这里设为V,需要线程B进行该锁的notify或notifyAll 并释放

  • (3)再同理,要想线程C走完,线程A接着走,那肯定是线程A有一把处于wait的锁,这里设为W,需要线程C进行该锁的notify或notifyAll 并释放

用图可以表示成下面的样子:

在这里插入图片描述
分析到这里我们可以再提炼一下:

  • (1)每个线程都应该有两把锁
  • (2)第一把锁是前面的线程释放后自己要抢到的锁、第二把锁是自己要notify或notifyAll的锁,对应到每个线程,就可以这样描述
    • 线程A需要两把锁,一把为线程C需要notify(或notifyAll)+ 释放的锁,可以认为该锁为C锁;另一把是自己需要notify(或notifyAll)+释放的锁,可以认为该锁为A锁
    • 同理,线程B需要A线程notify(或notifyAll)+ 释放的锁A锁,自己需要notify(或notifyAll)+释放的B锁
    • 再同理,线程C需要B线程notify(或notifyAll)+ 释放的锁B锁,自己需要notify(或notifyAll)+释放的C锁

分析到这里后,可以将上图改成下面的样子,这样理解起来,我感觉会更好一些:

在这里插入图片描述

分析到这里就可以写代码了。


2.1.3 我的答案

  • code
package com.nrsc.ch1.base.producer_consumer.ABCABC;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
@Slf4j
@AllArgsConstructor
public class ABCABC implements Runnable {

    private String obj;
    //前一个线程需要释放,本线程需要wait的锁
    private Object prev;
    //本线程需要释放,下一个线程需要wait的锁
    private Object self;


    @Override
    public void run() {
        int i = 3;
        while (i > 0) {
            //为了在控制台好看到效果,我这里打印3轮
            synchronized (prev) { //抢前面线程的锁
                synchronized (self) {// 抢到自己应该释放的锁
                    System.out.println(obj);
                    i--;
                    self.notifyAll(); //唤醒其他线程抢self
                }//释放自己应该释放的锁

                try {
                    //走到这里本线程已经释放了自己应该释放的锁,接下来就需要让自己需要等待的锁进行等待就可以了
                    if (i > 0) { //我最开始没加这个条件,但是测试发现程序没停,其实分析一下就可以知道
                        //当前面i--使i=0了,其实该线程就已经完成3次打印了,就不需要再等前面的锁了
                        //因此这里加了该if判断
                        prev.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object lockA = new Object();
        Object lockB = new Object();
        Object lockC = new Object();
        //线程A需要等待C线程释放的锁,同时需要释放本线程该释放的锁A
        new Thread(new ABCABC("A", lockC, lockA)).start();
        Thread.sleep(1); //确保开始时A线程先执行

        //线程B需要等待A线程释放的锁,同时需要释放本线程该释放的锁B
        new Thread(new ABCABC("B", lockA, lockB)).start();
        Thread.sleep(1); //确保开始时B线程第2个执行

        //线程C需要等待B线程释放的锁,同时需要释放本线程该释放的锁C
        new Thread(new ABCABC("C", lockB, lockC)).start();

    }
}
  • 测试结果:

在这里插入图片描述


2.2 生产者消费者问题


2.2.1 题目

如下图所示:

  • (1)有多个生产者,每个生产者都在不断的抢面包厂里的机器生产面包 —> 某个时间段只能有一个生产者进行生产
  • (2)厂里最多能存储20箱,也就是说当已经有20箱了,各个生产者就不能生产了,需要等待消费者消费了,才能继续生产
  • (3)消费者也有多个,他们也会抢着去面包厂买面包,但也是某个时间段,只能有一个消费者抢到买面包的资格

在以上条件的基础上,写一个多线程程序,保证在生产者不断生产面包的同时,消费者也在不断的购买面包。
注意: 不能写成生产者先生产了20箱,然后消费者再去消费20箱)
在这里插入图片描述


2.2.2 题目分析

其实我觉得这个很简单,只需要想明白下面的两点肯定就可以把这个代码写出来。

对于生产者

  • (1)它们要不停地生产,直到面包的箱数大于等于20时,生产者就等待 —> 等着消费者去消费
  • (2)当面包的箱数小于20时,抢到生产权的生产者就生产,并通知消费者,我刚生产了一个,你们可以再继续消费了

对于消费者

  • (1)他们要不停地消费,知道面包的箱数为0时,它们就等待 —> 等着生产这去生产
  • (2)当面包的箱数大于0时,抢到消费权的消费者就消费,并通知生产者,我刚消费了一个,你们可以再继续生产了

2.2.3 我的答案

  • 生产者和消费者
package com.nrsc.ch1.base.producer_consumer.multi;

import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BreadProducerAndConsumer2 {
    /***面包集合*/
    private int i = 0;

    /***
     * 生产者 ,注意这里锁是当前对象,即this
     */
    public synchronized void produceBread() {
        //如果大于等于20箱,就等待  --- 如果这里为大于20的话,则20不会进入while,则会生产出21箱,所以这里应为>=
        while (i >= 20) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                log.error("生产者{},等待出错", Thread.currentThread().getName(), e);
            }
        }

        //如果不到20箱就继续生产
        i++; //生产一箱
        log.warn("{}生产一箱面包,现有面包{}个", Thread.currentThread().getName(), i);
        //生产完,通知消费者进行消费
        this.notifyAll();
    }


    /***
     * 消费者
     */
    public synchronized void consumeBread() {

        //如果没有了就等待
        while (i <= 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                log.error("消费者{},等待出错", Thread.currentThread().getName(), e);
            }
        }
        //能走到这里说明i>0,所以进行消费
        i--; //消费一箱
        log.info("{}消费一个面包,现有面包{}个", Thread.currentThread().getName(), i);
        //消费完,通知生产者进行生产
        this.notifyAll();
    }
}
  • 测试类
package com.nrsc.ch1.base.producer_consumer.multi;
public class MultiTest {


    public static void main(String[] args) throws InterruptedException {

        BreadProducerAndConsumer2 pc = new BreadProducerAndConsumer2();

        /***
         * 不睡眠几秒,效果不是很好,
         * 因此我在
         *  生产者线程里睡了12秒 --- 因为我觉得生产面包的时间应该长 ☻☻☻
         *  消费者线程里睡了6秒  --- 因为我觉得买面包的时间应该快  ☻☻☻
         */

        //生产者线程
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                //每个线程都不停的生产
                while (true) {
                    try {
                        Thread.sleep(12);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pc.produceBread();
                }
            }, "生产者" + i).start();
        }


        //消费者线程
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                //每个线程都不停的消费
                while (true) {
                    try {
                        Thread.sleep(6);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pc.consumeBread();
                }
            }, "消费者" + i).start();
        }
    }

}
  • 测试效果如下:

在这里插入图片描述

发布了215 篇原创文章 · 获赞 280 · 访问量 49万+

猜你喜欢

转载自blog.csdn.net/nrsc272420199/article/details/104999423