Java线程通信/生产消费者模型(面试常问附练习题)

提示:本文带大家入门JavaSE基础的多线程部分,更深层次建议看书籍《Java并发编程》
全文主要内容为:
1.wait() notify() 的使用
2.生产消费者模型:面试常问
3.常见的问题
本人能力有限,如有遗漏或错误,敬请指正,谢谢


其他文章

1.Java多线程基本概念和常用API(面试高频)
2.线程安全问题(synchronized解决,各种类型全)
3.Java并发线程池使用和原理(通俗易懂版)
4.基于SpringBoot+Async注解整合多线程

前言

学习一门技术最好使用wwh方法
what:这门技术是什么
why:为什么用这个技术,使用会有什么优化
how:怎么使用


提示:以下是本篇文章正文内容,下面有代码案例可供参考,直接复制就能运行

一、前置知识

在这里插入图片描述

1.1 wait()方法

wait()是Object类的方法,调用wait()会导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量自动停止等待


大白话:wait()就是暂停当前正在执行的线程,并释放同步锁synchronized,让其他线程可以有机会运行
关键点:程序停顿在这一行,线程变为上图中的阻塞状态

具体怎么用看下面案例

1.2 notify()方法

notify()是Object类的方法,唤醒被wait()暂停的线程,比如线程A调用wait()暂停后,过了一段时间后想让他重新启动,就调用notify()。
不足:如果有多个线程被wait, 就唤醒优先级高的,也就是说不能指定唤醒某一个线程


大白话:员工请假(wait),老板叫他过来上班(notify)
关键点:线程状态从阻塞状态变为就绪状态

具体怎么用看下面案例

1.3 notifyAll()方法

和上面一样,只不过一旦执行此方法,就会唤醒所有被wait()的线程。

二、线程通信

正常情况下,每个子线程完成各自的任务就可以结束了。不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信

下面是举例说明:

2.1 两个线程轮流打印

需求:线程A打印A,线程B打印B,现在要求两个线程轮流打印十次
输出:ABABABABAB…

思路分析:对于“打印println()”这个动作,相当于共享变量,不可能两个线程随意执行,所以要加锁,轮流占有锁,这样就能轮流打印了

public static void main(String[] args) {
        //锁对象
        Object o = new Object();

        Thread A = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                synchronized (o) {
                    System.out.println("A");
                }
            }
        });
        Thread B = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                synchronized (o) {
                    System.out.println("B");
                }
            }
        });
        A.start();
        B.start();
    }

那么现在加锁后,能保证同一时间只有一个线程在打印(谁持有锁就谁打印)
现在问题在于如何释放锁,并且释放锁后,保证锁由另外一个线程获取到(因为两个线程都可以抢占锁,即使是刚刚释放锁的线程)

思路分析:使用上面的wait()和notify(),使用wait()可以释放锁。

  1. 假设现在A拿到锁,打印完“A”后,调用wait()方法,自己会处于阻塞状态,无法抢占锁。另一个线程B这时候就一定能拿到锁(因为A线程处于阻塞)
  2. 等拿到锁打印完“B”后,就执行notify()唤醒线程A,由于唤醒线程A后,锁还是B拿着,A进不去,这时候B就要调用wait()方法,释放锁同时自己阻塞

所以只需要记住:业务-唤醒-释放锁等待

public static void main(String[] args) {
        //锁对象
        Object o = new Object();
        
        Thread A = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                synchronized (o) {
                    //1.业务
                    System.out.println("A");
                    //2.唤醒(调用者为什么是o这个对象,看下文)
                    o.notify();
                    //3.释放锁等待:wait()方法是需要捕获异常的
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread B = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                synchronized (o) {
                    //1.业务
                    System.out.println("B");
                    //2.唤醒
                    o.notify();
                    //3.释放锁等待:wait()方法是需要捕获异常的
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        A.start();
        B.start();
    }

2.2 常见面试题

1.wait()和notify()的调用者必须是锁对象,并且只能在synchronized内使用
答:调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。如果wait()方法在外部使用的话,会报异常
在这里插入图片描述

2.wait方法释放锁,被唤醒后,醒来的位置就是wait所在的位置

3.sleep()和wait()区别

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

2.sleep是使线程休眠,不会释放对象锁;wait是使线程等待,释放锁,sleep让出的是cpu,如果此时代码是加锁的,那么即使让出了CPU,其他线程也无法运行,因为没有得到锁;wait是让自己暂时等待,放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

3.sleep()可以用在任何位置,wait()只能用在同步代码块或者同步方法中

三、生产消费者模型(经典面试手写※)

需求:现在有一个生产者生产商品(商品增加一个),当商品数量大于0的时候,消费者就会消费(商品减少一个),循环十次

思路:业务-唤醒-释放锁等待
在实际多线程开发的时候,资源类和线程类是分开的,不要直接把业务写在run()方法中

编写商品类:商品里有增加商品和减少商品方法

public class Product {
    //商品数量
    public int count;
	
    public Product(int count){
        this.count=count;
    }

    //增加商品:同步方法
    public synchronized void add(){
    	//商品数量>0说明生产者不需要干任何事,所以如果抢到锁就直接等待
        if(this.count>0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //等生产者被唤醒后代码就会到这里,说明要生产了
        System.out.println("生产者开始生产商品,当前商品数量为:"+this.count);
        this.count++;
        System.out.println("生产者生产完商品,当前商品数量为:"+this.count);
        System.out.println("------------");
        //生产完通知消费者消费
        this.notify();
    }

    //消费商品:同步方法
    public synchronized void pop(){
    	//当商品<=0的时候,所以没有商品,消费者无法消费,所以如果抢到锁就等待
        if(this.count<=0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //被唤醒后,有商品了所以开始消费
        System.out.println("消费者开始消费商品,当前商品数量为:"+this.count);
        this.count--;
        System.out.println("消费者消费商品后,当前商品数量为:"+this.count);
        System.out.println("------------");
  		this.notify();
public static void main(String[] args) {
		//假设现在有5个商品,那么也就是说线程启动,一定是消费者先消费
        Product product=new Product(5);
        
        //生产者线程
        Thread producer=new Thread(() -> {
            for(int i=0;i<10;i++){
               //线程类和资源类分开,只需要调用业务的方法
               product.add();
            }
        },"生产者");
		//消费者线程
        Thread consumer=new Thread(() -> {
            for(int i=0;i<10;i++){
                product.pop();
            }
        },"消费者");
        producer.start();
        consumer.start();
    }

在这里插入图片描述

可以看到当初始化的商品有5个的时候,那么生产者是不会做任何操作的(因为执行了wait()方法)
当消费者消费商品到0的时候,生产者开始生产,之后由于生产者每生产一个,消费者感知到就消费了,所以最后面是交替执行

四、多个生产消费者模型可能出现的问题:虚假唤醒

现在假设有两个生产者,两个消费者 ,场景和上面一样

//新增生产者B,消费者B
new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    product.push();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "生产者B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    product.pop();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者B").start();

实际运行结果:没有同步的效果

生产者A添加产品,剩余1件产品
消费者A使用产品,剩余0件产品
生产者B添加产品,剩余1件产品
生产者A添加产品,剩余2件产品
生产者B添加产品,剩余3件产品
消费者A使用产品,剩余2件产品
消费者A使用产品,剩余1件产品
...

4.1 虚假唤醒

问题在于线程被唤醒后的位置=wait()方法调用的位置,就是从哪里等待就从哪里醒来。

		if(this.count>0){
            try {
                this.wait();//线程被唤醒后,从这里继续执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

那么现在场景如下:
1.生产者A抢到锁生产产品(产品数量:1),释放锁后,生产者A又抢到锁,由于产品数量为1,进入if语句进行等待。
2.然后消费者A抢到锁进行消费(产品数量:0)
3.生产者B抢到锁进行生产(产品数量:1),此时释放锁。
4.生产者A抢到锁,这时候关键来了:生产者醒来的位置在if语句的下面,重新抢到时间片进行方法调用也是从这里开始,然后由于不用进行if判断,又可以生产了(产品数量:2)

//现在是生产者A抢到锁进入方法:此时产品数量为1
		if(this.count>0){
            try {
                this.wait();//生产者A在这里醒来,抢到时间片后继续从这里开始执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //添加产品代码:生产者A又生产了一个产品
        

解决办法:if改成while,这样就算醒来也还是要再判断一次

		while(this.count>0){
            try {
                this.wait();//生产者A在这里醒来,抢到时间片后,由于是while循环会继续判断
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

总结:两个线程(生产者+消费者)不会出现虚假唤醒问题,四个或多个线程才会出现

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值