【并发编程】 --- 线程间的通信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();
        }
    }

}
  • 测试效果如下:

在这里插入图片描述

  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
### 回答1: Java线程可以通过以下方式进行通信: 1. 使用 wait() 和 notify() 方法,这需要使用同步代码块或同步方法。在同步代码块或同步方法中,线程可以调用 wait() 方法阻塞,并在其他线程调用 notify() 方法后恢复执行。 2. 使用 CountDownLatch,它是一个同步辅助类,可以让一个线程等待其他线程的完成。 3. 使用 CyclicBarrier,它也是一个同步辅助类,可以让多个线程在某个点上等待,直到所有线程都到达。 4. 使用 Executor 框架,该框架可以在不同的线程创建任务和管理线程。 5. 使用 BlockingQueue,它是一种阻塞队列,可以在生产者和消费者线程进行通信。 以上是Java线程通信的一些常用方法,您可以根据您的需求选择适当的方法进行实现。 ### 回答2: Java线程可以通过以下几种方式进行通信: 1. 共享变量:多个线程可以通过读写共享变量来进行通信。可以通过使用synchronized关键字来确保共享变量的原子性操作,避免多线程并发访问导致的数据不一致。 2. wait()和notify()方法线程可以通过调用Object类中的wait()方法让自己进入等待状态,等待其他线程调用notify()或notifyAll()方法唤醒。这样可以实现线程的同步和互斥,实现线程通信。 3. Lock和Condition:Java提供了Lock和Condition接口,可以通过它们实现更灵活的线程通信。通过调用Lock对象的lock()方法获取锁,在某个条件满足时调用Condition的await()方法进入等待状态,直到其他线程调用signal()或signalAll()方法唤醒。这种方式相比于wait()和notify()方法更加灵活可控。 4. 管道通信:可以使用PipedInputStream和PipedOutputStream、PipedReader和PipedWriter等管道类进行线程通信。通过一个线程往管道中写数据,另一个线程从管道中读取数据,实现线程的数据传输。 5. 使用阻塞队列:Java提供了阻塞队列(如ArrayBlockingQueue、LinkedBlockingQueue等)作为线程间通信的工具,线程可以通过put()方法往队列中放入数据,通过take()方法从队列中获取数据。当队列为空时,take()方法会阻塞线程,直到队列有数据时再唤醒线程。 总之,Java线程通信可以通过共享变量、wait()和notify()方法、Lock和Condition、管道通信以及阻塞队列等方式来实现。不同的方式适用于不同的场景,开发者需要根据具体需求选择合适的通信方式。 ### 回答3: 在Java中,线程可以通过以下几种方式进行通信: 1. 共享变量:线程可以通过共享变量来进行通信。多个线程可以共同访问和修改同一个变量,来实现数据的传递和共享。但是需要注意的是,在使用共享变量时需要保证多线程的互斥性,否则可能会发生数据竞争问题。 2. 等待/通知机制:Java提供了Object类的wait()、notify()和notifyAll()方法线程可以使用这些方法来进行等待和通知。线程可以调用wait()方法进入等待状态,直到其他线程调用该对象的notify()或notifyAll()方法来唤醒等待的线程。等待/通知机制可以用于线程的一对一或者一对多的通信。 3. 使用阻塞队列:Java中的阻塞队列可以作为线程间通信的一种方式,线程可以将数据放入队列中,等待其他线程从队列中获取数据。线程可以使用put()方法将元素放入队列中,使用take()方法从队列中获取元素。阻塞队列的特点是当队列为空时,获取元素的操作会被阻塞,直到有其他线程放入元素;当队列已满时,放入元素的操作会被阻塞,直到有其他线程取出元素。 4. 使用信号量:Java中的Semaphore类提供了信号量机制,用于控制同时访问某个资源的线程数。信号量维护了一个计数器,线程可以在访问资源之前调用acquire()方法获取一个许可,许可数会随之减少;在使用完资源后,线程需要调用release()方法释放许可,许可数会增加。通过控制许可的数量,可以实现线程的同步和通信。 总的来说,Java提供了多种机制来实现线程通信,开发者可以根据具体需求选择合适的方式。但是在进行线程间通信时,需要注意线程安全的问题,避免出现竞态条件和死锁等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值