1.简介
在这篇文章中,我们将看一下java的最基础的机制-线程同步。我们将首先讨论一些最基本的并发-相关的术语和方法论。同时,我们也会创建一个简单得应用-在这个应用中,我们将处理并发问题,比便于我们能更好地理解wait()和notify()方法。
2.java中的线程同步
在多线程环境中,多个线程可能试图修改同一个资源。如果线程没有被很好地管理,这肯定会引发一致性问题。
2.1 java中的守护块
在java中,我们可以用来协调多线程动作的工具----是守护块,这个代码块会在继续执行之前先检查一个特定的条件是否满足。
在脑海中记着那一点,我们将利用:
.Object.wait() -挂起一个线程
.Object.notify() -唤醒一个线程
从下面的图中将会是一个很好的理解,该图描画出了一个线程的生命周期:
请注意,这里有很多控制该生命周期的方式,然而,在本文中,我们仅聚焦于wait和notify()方法。
3.wait()方法
简单地说,当我们调用wait()方法时----它会强制当前线程一直等待直到其他线程在调用同一对象上的notify()或notifyAll()方法。
为此,当前线程必须拥有该对象的监视器。根据javaDoc文档的说明,在以下情况下,会发生:
.针对给定的对象,我们已经执行了同步的实例方法
.我们已经执行了给定对象上的同步代码块
.通过为Class类型对象的执行同步静态方法
请注意:在某一时刻,只有一个活跃的线程可以拥有一个对象的监视器。
wait()方法会伴随有三个重载的签名。下面我们来看一下。
3.1 wait()
wait()方法会导致当前线程无限期地等待直到另一个线程调用该对象的notify()或notifyAll()方法。
The wait() method causes the current thread to wait indefinitely until another thread either invokes notify() for this object or 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()方法是唤醒单一的随机线程,所以它可以用于实现互斥排他锁(针对于多个线程在做相似的任务)。使用notify()实现notifyAll()方法
更加切实可行。
4.2 notifyAll()方法
该方法会唤醒所有等待当前对象的监视器的所有线程。被唤醒的线程将会按照通常的方式完成运行---就像其他线程一样。
但是,在我们允许他们的运行继续之前,总是先快速检查一下该线程继续进行所必需的条件---因为还有一些情况时,线程在没有收到任何通知的情况下被唤醒。
5.Sender-Receiver 同步问题
既然我们理解了这些基础概念,让我们搞一个简单得Sender-Receiver应用-在这个应用中会利用wait()和notify()方法在发送者和接收者之家设置同步:
.Sender会发送一个数据包给接收者
.接收者不能处理数据包直到Sender完成发送
.类似地,Sender不能发送另一个数据包除非Receiver已经处理完了前一个数据包。
我们先创建一个Data类,这个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();
returnpacket;
}
}
我们来停下来,看看我们都干了什么:
. packet变量表明了正在网络上传输的数据。
. 我们有一个布尔变量transfer- Sender发送者和Receiver接收者将使用它来做同步:
.如果该变量为true,Receiver就会等待发送者发送消息。
.如果该变量为false,Sender发送者就会等待接收者去接收消息。
.Sender发送者会使用send()方法把数据发送给Receiver接收者
.如果transfer为false,我们将在当前线程上调用wait()方法去等待
.但是当它为true时,我们会改变其状态,设置我们的消息message,并且调用notifyAll()去唤醒其他线程,告诉它们有一个
重要的事件发送了,同时这些被唤醒的线程会检查一下它们是否能继续运行。
.相似地,Receiver将使用receive()方法:
.如果该transfer被Sender设置为false,那么只有它能继续运行,否则的话,我们将在该线程上调用wait()
.当条件满足时,我们再次改变状态,提醒所有等待的线程赶紧醒来,并且把数据包返回。
5.1 为什么在while循环中关闭wait()???
由于notify()和notifyAll()方法会随机唤醒正在等待该对象监视器的线程,满足条件并不总是重要。有时会发生这种情况:该线程已经被唤醒了,但是条件还没有得到满足。
我们也可以定义一个检查以避免虚假的唤醒 --线程可以在没有收到任何提醒的情况下唤醒。
We can also define a check to save us from spurious wakeups – where a thread can wake up from waiting without ever having received a notification。
5.2 为什么我们要同步send()和receive()方法???
我们把这些方法置于同步方法中以提供内置锁intrinsic locks.如果一个调用wait()方法的线程没有拥有内置锁,就会抛出一个错误。
我们现在讲创建一个Sender和Receiver并且都实现Runnable接口,只有那样,它们的实例才能被一个线程执行。
我们先来看一下Sender是如何工作的:
public class Sender implements Runnable {
privateData data;
// standard constructors
publicvoidrun() {
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);
}
}
}
}
针对这个Sender:
.我们创建了一些随机的数据包-这些数据包将会在网络上被传输。
.对于每一个packert,我们单纯地就调用了一个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 = newData();
Thread sender = newThread(newSender(data));
Thread receiver = newThread(newReceiver(data));
sender.start();
receiver.start();
}
我们将得到下面的输出结果:
First packet
Second packet
Third packet
Fourth packet
到了这里,我们已经正确地接收到了所有的数据包,并且是按照序列顺序,并且成功地在发送者Sender和receiver接收者之间建立了正确的通信。
6.总结
在这篇文章中,我们讨论了java中同步的一些核心概念,更特别的是,我们聚焦于如何使用wait()和notify()方法去解决同步问题。并且最后我们在实际中应用了这些概念。
在继续往下深入之前,我们有必要提一提这些低级别的API,例如: wait、notify()、notifyAll()方法。但是更高解绑的机制通常更加简单高效-例如 java的本地锁Lock以及Condition接口(可以在java.util.concurrent.locks包中找到)。
object.wait()、object.notify()方法上的object的作用