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()
方法向接收者发送数据:- 如果
transfer
为false
,我们将通过在此线程上调用wait()
来等待。 - 但当它为
true
时,我们切换状态,设置我们的消息,并调用notifyAll()
来唤醒其他线程,指出已经发生了一个重要事件,他们可以检查是否可以继续执行。
- 如果
- 同样,接收者将使用
receive()
方法:- 如果发送者将
transfer
设置为false
,那么它才会继续,否则我们将在此线程上调用wait()
。 - 当条件满足时,我们切换状态,通知所有等待的线程唤醒,并返回接收到的数据包。
- 如果发送者将
5.1. 为什么要在while循环中封装wait()方法
由于 notify()
和 notifyAll()
随机唤醒在此对象监视器上等待的线程,满足条件并不总是重要的。有时线程被唤醒,但条件实际上还没有满足。
我们也可以定义一个检查来保护我们免受虚假唤醒 —— 在这种情况下,一个线程可以在从未接收到通知的情况下从等待中唤醒。
5.2. 我们为什么需要同步 send() 和 receive() 方法?
我们将这些方法放在同步方法内部以提供内置锁。如果调用 wait()
方法的线程不拥有内置锁,将会抛出错误。
现在我们将创建 Sender
和 Receiver
,并在两者上实现 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的原生 Lock
和 Condition
接口(在 java.util.concurrent.locks
包中可用)。
评论区