• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
思想人生从关注生活开始
博客园    首页    新随笔    联系   管理    订阅  订阅

Java中的wait与notify方法-Java快速入门教程

1. 概述

在本教程中,我们将介绍 Java 中最基本的机制之一 — 线程同步。

我们将首先讨论一些与并发相关的基本术语和方法,接着将开发一个简单的应用程序来处理并发问题,目的是更好地理解wait()和notify()。

2. Java 中的线程同步

在多线程环境中,多个线程可能会尝试修改同一资源。当然,不正确地管理线程会导致一致性问题。

2.1. Java 中的受保护块

我们可以用来协调 Java 中多个线程操作的一个工具是受保护的块。此类块在恢复执行之前会检查特定条件。

考虑到这一点,我们将利用以下内容:

  • Object.wait()挂起线程
  • Object.notify()唤醒线程

我们可以从下图中更好地理解这一点,该图描述了线程的生命周期:

请注意,有许多方法可以控制此生命周期。但是,在本文中,我们将只关注wait() 和notify()。

3.wait() 方法

简单地说,调用wait()强制当前线程等待,直到其他线程在同一对象上调用notify()或notifyAll()。

为此,当前线程必须拥有对象的监视器。根据Javadocs的说法,这可以通过以下方式发生:

  • 当我们为给定对象执行同步实例方法时
  • 当我们在给定对象上执行同步块的主体时
  • 通过对类类型的对象执行同步静态方法

请注意,一次只有一个活动线程可以拥有对象的监视器。

这个wait()方法带有三个重载的签名。让我们来看看这些。

3.1.wait() 

wait() 方法使当前线程无限期等待,直到另一个线程为此对象调用notify() 或notifyAll()。

3.2.wait(long timeout)

使用此方法,我们可以指定一个超时,在此之后线程将自动唤醒。线程可以在达到超时之前使用 notify() 或notifyAll() 唤醒。

请注意,调用wait(0) 与调用wait() 相同。

3.3.wait(long timeout, int nanos)

这是另一个提供相同功能的签名。这里唯一的区别是我们可以提供更高的精度。

总超时周期(以纳秒为单位)计算为 1_000_000*timeout + nanos。

4.notify() 与notifyAll()

我们使用notify() 方法来唤醒正在等待访问此对象监视器的线程。

有两种方法可以通知等待线程。

4.1.notify() 

对于在此对象的监视器上等待的所有线程(通过使用任何一个wait()方法),方法通知 notify() 通知其中任何一个线程任意唤醒。确切地选择要唤醒的线程是不确定的,取决于实现。

由于notify() 唤醒了一个随机线程,我们可以使用它来实现线程执行类似任务的互斥锁定。但在大多数情况下,实现notifyAll() 会更可行。

4.2.notifyAll()

此方法只是唤醒正在等待此对象监视器的所有线程。

唤醒的线程将以通常的方式竞争,就像尝试在此对象上同步的任何其他线程一样。

但在我们允许它们继续执行之前,请始终定义一个快速检查,以检查继续线程所需的条件。这是因为在某些情况下,线程在没有收到通知的情况下被唤醒(此示例稍后将讨论此方案)。

5. 发送-接收同步问题

现在我们了解了基础知识,让我们通过一个简单的发送方-接收方应用程序,该应用程序将使用wait() 和notify() 方法来设置它们之间的同步:

  • 发送方应该向接收方发送数据包。
  • 在发送方完成发送之前,接收方无法处理数据包。
  • 同样,发送方不应尝试发送另一个数据包,除非接收方已经处理了前一个数据包。

让我们首先创建一个Data类,该类由将从发送方发送到接收方的数据包组成。我们将使用wait() 和notifyAll() 来设置它们之间的同步:

public class Data {
    private String packet;
    
    // True if receiver should wait
    // False if sender should wait
    private boolean transfer = true;
 
    public synchronized String receive() {
        while (transfer) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); 
                System.err.println("Thread Interrupted");
            }
        }
        transfer = true;
        
        String returnPacket = packet;
        notifyAll();
        return returnPacket;
    }
 
    public synchronized void send(String packet) {
        while (!transfer) {
            try { 
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); 
                System.err.println("Thread Interrupted");
            }
        }
        transfer = false;
        
        this.packet = packet;
        notifyAll();
    }
}

让我们分解一下这里发生的事情:

  • 数据包变量表示通过网络传输的数据。
  • 我们有一个布尔变量传输,发送方和接收方将使用它进行同步:
    • 如果此变量为true,则接收方应等待发送方发送消息。
    • 如果为false,发送方应等待接收方接收消息。
  • 发送方使用send() 方法将数据发送到接收方:
    • 如果传输为假,我们将通过在此线程上调用wait() 来等待。
    • 但是当它为true 时,我们切换状态,设置我们的消息,并调用notifyAll() 来唤醒其他线程以指定发生了重大事件,它们可以检查是否可以继续执行。
  • 类似地,接收器将使用receive() 方法:
    • 如果传输被Sender 设置为false,则只有这样它才会继续,否则我们将在此线程上调用wait()。
    • 当满足条件时,我们切换状态,通知所有等待的线程唤醒,并返回收到的数据包。

5.1. 为什么要在wait循环中加入wait()?

由于notify() 和notifyAll() 随机唤醒正在此对象监视器上等待的线程,因此满足条件并不总是很重要。有时线程被唤醒,但条件实际上尚未满足。

我们还可以定义一个检查来避免虚假唤醒——线程可以在没有收到通知的情况下从等待中唤醒。

5.2. 为什么我们需要同步end() 和receive()方法?

我们将这些方法放在同步方法中以提供内部锁。如果调用wait() 方法的线程不拥有固有锁,则会引发错误。

现在,我们将创建发送方和接收方,并在两者上实现Runnable接口,以便它们的实例可以由线程执行。

首先,我们将看到发件人将如何工作:

public class Sender implements Runnable {
    private Data data;
 
    // standard constructors
 
    public void run() {
        String packets[] = {
          "First packet",
          "Second packet",
          "Third packet",
          "Fourth packet",
          "End"
        };
 
        for (String packet : packets) {
            data.send(packet);

            // Thread.sleep() to mimic heavy server-side processing
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); 
                System.err.println("Thread Interrupted"); 
            }
        }
    }
}

让我们仔细看看这个发件人:

  • 我们正在创建一些随机数据包,这些数据包将以数据包[]数组的形式通过网络发送。
  • 对于每个数据包,我们只是调用 send()。
  • 然后我们以随机间隔调用Thread.sleep() 来模拟繁重的服务器端处理。

最后,让我们实现我们的接收器:

public class Receiver implements Runnable {
    private Data load;
 
    // standard constructors
 
    public void run() {
        for(String receivedMessage = load.receive();
          !"End".equals(receivedMessage);
          receivedMessage = load.receive()) {
            
            System.out.println(receivedMessage);

            //Thread.sleep() to mimic heavy server-side processing
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); 
                System.err.println("Thread Interrupted"); 
            }
        }
    }
}

在这里,我们只是在循环中调用load.receive(),直到我们得到最后一个“End”数据包。

现在让我们看看这个应用程序的运行情况:

public static void main(String[] args) {
    Data data = new Data();
    Thread sender = new Thread(new Sender(data));
    Thread receiver = new Thread(new Receiver(data));
    
    sender.start();
    receiver.start();
}

我们将收到以下输出:

First packet
Second packet
Third packet
Fourth packet

我们来了。我们以正确的顺序接收所有数据包,并成功在发送方和接收方之间建立了正确的通信。

6. 结论

在本文中,我们讨论了 Java 中的一些核心同步概念。更具体地说,我们专注于如何使用wait()和notify()来解决有趣的同步问题。最后,我们浏览了一个代码示例,在实践中应用了这些概念。

在我们结束之前,值得一提的是,所有这些低级 API,如wait()、notify() 和notifyAll(),都是运行良好的传统方法,但更高级别的机制通常更简单、更好——例如 Java 的原生Lock和Condition接口(在java.util.concurrent.locks包中可用)。

posted @ 2023-02-14 17:03  JackYang  阅读(558)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3