侧边栏壁纸
博主头像
翻斗

开始一件事最好是昨天,其次是现在

  • 累计撰写 44 篇文章
  • 累计创建 42 个标签
  • 累计收到 2 条评论

Java中的wait和notify方法

翻斗
2020-03-03 / 0 评论 / 0 点赞 / 547 阅读 / 4,812 字

Baeldung 翻译系列之Java并发基础:

Java中的concurrent包
Java中的Synchronized关键字
Future介绍
ThreadLocal介绍
Java线程的生命周期
如何杀掉一个Java线程
Java中的线程池介绍
实现Runnable接口还是继承Thread类
Java中的wait和notify方法
Runnable vs Callable
wait和sleep的区别
Thread.join方法介绍
Java中使用锁对象
ThreadPoolTaskExecutor中的corePoolSize和maxPoolSize
Java中的异步编程

1. 概览

在这个教程中,我们将探讨 Java 中最基本的机制之一 —— 线程同步。

我们首先将讨论一些基本的并发相关的术语和方法。

我们将开发一个简单的应用程序,在这个应用程序中,我们将处理并发问题,目标是更好地理解 wait()notify()

2. Java中的线程同步

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

2.1. Java 中的“保护块

我们可以使用保护块来协调 Java 中多个线程的行为, 这样的块在恢复执行之前会检查特定的条件。

考虑到这一点,我们将使用以下方法:

Object.wait() 用于挂起线程
Object.notify() 用于唤醒线程
我们可以从下面描述线程生命周期的图表中更好地理解这一点:

请注意,有许多方法可以控制这个生命周期。然而,在这篇文章中,我们只关注 wait()notify()

3. wait()方法

简单来说,调用 wait() 会让当前线程等待,直到其他线程在同一个对象上调用 notify()notifyAll()

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

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

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

这个 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();
    }
}

让我们详解一下这里发生了什么:

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

5.1. 为什么要在while循环中封装wait()方法

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

我们也可以定义一个检查来保护我们免受虚假唤醒 —— 在这种情况下,一个线程可以在从未接收到通知的情况下从等待中唤醒。

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

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

现在我们将创建 SenderReceiver,并在两者上实现 Runnable 接口,以便它们的实例可以被线程执行。

首先,我们将看一下 Sender 是如何工作的:

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"); 
            }
        }
    }
}

让我们更仔细地看一下这个 Sender

  • 我们在 packets[] 数组中创建了一些将被发送到网络的随机数据包。
  • 对于每个数据包,我们只是调用 send()
  • 然后我们调用 Thread.sleep(),间隔随机,以模拟服务器端的重型处理。
    最后,让我们实现我们的 Receiver
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的原生 LockCondition 接口(在 java.util.concurrent.locks 包中可用)。

和往常一样,本文中使用的完整代码片段可以在GitHub 或者 Gitee上找到。

0

评论区