1.简介
在本文中,我们将介绍Java中最基本的机制之一 - 线程同步。
我们将首先讨论一些基本的并发相关术语和方法。
我们将开发一个简单的应用程序 - 我们将处理并发问题,目的是更好地理解wait()和notify()。
2. Java中的线程同步
在多线程环境中,多个线程可能会尝试修改同一资源。如果未正确管理线程,这当然会导致一致性问题。
2.1 Java中的守卫块
我们可以用来协调Java中多个线程的操作的一个工具是受保护的块。这些块在恢复执行之前会检查特定条件。
考虑到这一点,我们将利用:
- Object.wait() - 挂起一个线程
- Object.notify() -唤醒线程
从下图中可以更好地理解这一点,该图描绘了Thread的生命周期:
请注意,有许多方法可以控制这个生命周期; 但是,在本文中,我们将只关注wait()和notify()。
3. wait()方法
简单地说,当我们调用wait()时 -这会强制当前线程等待,直到某个其他线程在同一个对象上调用notify()或notifyAll()。
为此,当前线程必须拥有对象的监视器。根据Javadocs的说法,这可能发生在:
- 我们已经为给定对象执行了同步实例方法
- 我们在给定对象上执行了synchronized块的主体
- 通过为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.发送者 - 接收者同步问题
现在我们已经了解了基础知识,让我们通过一个简单的Sender - Receiver应用程序 - 它将使用wait()和notify()方法来设置它们之间的同步:
- 发件人应该将数据分组发送到接收器
- 接受者不能处理数据包,直到发送者将发送的数据包发送完成
- 同样,发送方不得尝试发送另一个数据包,除非接收方已处理过上一个数据包
让我们首先创建一个Data类,它包含将从Sender发送到Receiver的数据包。我们将使用wait()和notifyAll()来设置它们之间的同步:
public class Data {
private String packet;
// True if receiver should wait
// False if sender should wait
private boolean transfer = true;
public synchronized void send(String packet) {
while (!transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
transfer = false;
this.packet = packet;
notifyAll();
}
public synchronized String receive() {
while (transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
transfer = true;
notifyAll();
return packet;
}
}
让我们分解这里发生的事情:
- 变量数据包表示正在通过网络传输数据
- 我们有一个布尔变量transfer -将发件人和接收器将使用同步:
- 如果此变量为true,则Receiver应等待Sender发送消息
- 如果它是false,那么Sender应该等待Receiver接收消息
- 发送者使用send()方法将数据发送到接收器:
- 如果transfer为false,我们将在此线程上调用wait()
- 但是当它为true时,我们切换状态,设置我们的消息并调用notifyAll()来唤醒其他线程以指定发生重大事件并且他们可以检查它们是否可以继续执行
- 同样,Receiver将使用receive()方法:
- 如果transfer 设置为false的发件人,则会进行,否则我们将调用这个线程的wait()
- 满足条件时,我们切换状态,通知所有等待线程唤醒并返回Receiver的数据包
5.1 为什么在while循环中包含wait()?
由于notify()和notifyAll()随机唤醒正在此对象监视器上等待的线程,因此满足条件并不总是很重要。有时可能会发生线程被唤醒,但实际上并没有满足条件。
我们还可以定义一个检查来避免虚假的唤醒 - 一个线程可以在没有收到通知的情况下从等待中醒来。
5.2 为什么我们需要同步send()和receive()方法?
我们将这些方法放在synchronized方法中以提供内部锁。如果调用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();
Log.error("Thread interrupted", e);
}
}
}
}
对于这个发件人:
我们正在创建一些随机数据包,这些数据包将通过网络以packet []数组的形式发送
对于每个数据包,我们只是调用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);
// ...
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
}
}
在这里,我们只是在循环中调用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包中提供)。
有关java.util.concurrent包的更多信息,请访问我们对java.util.concurrent文章的概述,此处的java.util.concurrent.Locks指南中介绍了Lock和Condition。
关注公众号:「Java知己」,每天更新Java知识哦,期待你的到来!
- 发送「1024」,免费领取 30 本经典编程书籍。
- 发送「Group」,与 10 万程序员一起进步。
- 发送「JavaEE实战」,领取《JavaEE实战》系列视频教程。
- 发送「玩转算法」,领取《玩转算法》系列视频教程。