Java中是使用while语句来检查条件,使用wait方法来执行等待的。当条件发生变化时,使用notify/notifyAll方法发出通知,这就是Guarded Suspension模式。
在这个程序中,一个线程(ClientThread)会将请求(Request)的实例传递给另一个线程(ServerThread)。
名字 | 说明 |
---|---|
Request | 表示一个请求的类 |
RequestQueue | 依次存放请求的类 |
ClientThread | 发送请求的类 |
ServerThread | 接受请求的类 |
Main | 测试程序行为的类 |
Request类用于表示请求,用于表示CilentThread传递给ServerThread的实例。Request类只有一个名称属性(name字段)。
public class Request {
private final String name;
public Request(String name){
this.name=name;
}
public String getName(){
return name;
}
public String toString(){
return "[Request"+name+"]";
}
}
RequestQueue类用于依次存放请求。定义了putRquest和getRequest,先进先出,这种结构通常称为队列(queue)
import java.util.LinkedList;
import java.util.Queue;
public class requestQueue {
private final Queue<Request> queue=new LinkedList<Request>();//队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作
public synchronized Request getRequest(){ //LinkedList类实现了Queqe接口,因此我们可以把LinkedList当成Queue来使用
while (queue.peek()==null){ //element()和peek()用在队列的头部查询元素,与remove()方法类似,在队列为空时,element()抛出一个异常,而peek()返回null
try {
wait();
}catch (InterruptedException e){
}
}
return queue.remove();//删除队列的第一个元素,Queue 中 remove() 和 poll()都是用来从队列头部删除一个元素。
//在队列元素为空的情况下,remove() 方法会抛出NoSuchElementException异常,poll() 方法只会返回 null 。
}
public synchronized void putRequest(Request request){
queue.offer(request);//向队列添加一个元素,也就是添加request,异常会抛出false
notifyAll();//唤醒在此对象监视器上等待的所有线程。
}
}
Request peek()
如果队列中存在元素,则该方法会返回头元素;如果为空,则返回null。在这里getRequest的的目的是“从queue中取出一个Request实例”,也就是为了执行queue.remove。但是提前条件必须是“存在想要取出的元素”,这种必须要满足的条件就称为Guard Suspension模式的守护条件。我认为该模式的核心也就是要做好守护条件的编写和维护。
执行wait,等待条件发生变化
当守护条件不成立时,进行等待,他是在等待着什么呢?是在等待着“notify/notifyAll”。是这样的吗,线程真正在等待的是实例状态的变化,线程之所以在等待,是因为守护条件没满足。只有知道线程在等待着什么,才会明白应该何时执行notify/notifyAll。
在Guarded Suspension模式中,使用while来检查守护条件是非常重要的。notify/notifyAll只不过是检查守护条件的触发器而已。
将wait替换成Thread.sleep可以吗
不可以,会发生问题。wait与Thread.sleep不同,执行wait的线程会释放对象实例的锁,而Thread.sleep不会释放实例的锁。因此如果getRequest方法一直持有锁,其他线程无法进入putRequest,守护状态一直不会改变,所以就陷入了不断地死循环,失去了生存性。
将whlie换成if可以吗
不可以。这种情况在本例不会发生问题,但是一般情况还是会有问题。
while语句属于循环语句,在判断是,如果条件为true,则会继续判断,直到false为止,即会进行多次判断。
if语句属于条件判断语句,如果条件是true,则继续执行,为false则跳出语句不执行,只会进行单次判断。
延伸到本例来看,使用if, 假设queue中只有一个元素,线程全部notifyAll之后,第一个开始运行的线程调用queue.remove()之后,queue会变成空,若为空接下来的线程因为守护条件就无法进行,可是如果之后有几个线程中,已经判断守护条件成立,那也会调用remove,这样就会出现问题,if不会再次判断了,while会再次判断就不会出现问题。就像下面举得这个例子一样。
试想一下这个场景,假如你在开车,遇到红灯后就停下来,坐在那里发呆,当你的副驾驶突然提醒你可以走了,你也不能马上踩油门来走,而是确认一下等已经变绿之后才可以开车,if和while地区别就在这里。
Request remove()
该方法用于移除队列的第一个元素,并返回该元素。如果队列中一个元素都没有,则会抛出异常。pull()也是有相同的作用,只是当队列元素为空时,只会返回null。
queue.offer(request)与notifyAll()能否颠倒顺序?
可以,在执行notifyAll()之后,即使其他线程被唤醒了,但是由于此线程还拥有着锁,其他线程会阻塞,没有什么进展,只有全部执行完以后,才会返回。不过,将notifyAll()写在后面更容易理解,实例变化了,才因此执行唤醒操作
RequestQueue类用于表示发送请求的线程。ClientThread持有RequestQuene的实例(requestQuene),并连续调用该实例的putRequest,放入请求。为了错开发送请求(执行putRequest)的时间点,用了sleep。
import java.util.Random;
public class ClientThread extends Thread{
private final Random random;
private final requestQueue requestQueue;
public ClientThread(requestQueue requestQueue,String name,long seed){
super(name);
this.requestQueue=requestQueue;
this.random=new Random(seed);
}
@Override
public void run() {
for (int i=0;i<10000;i++) {
Request request = new Request("NO." + i);
System.out.println(Thread.currentThread().getName() + " requests " + request);
requestQueue.putRequest(request);
try {
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
}
}
}
}
RequestQueue类用于表示接受请求的线程。ServerThread持有RequestQuene的实例(requestQuene),并连续调用该实例的getRequest,放入请求。为了错开发送请求(执行getRequest)的时间点,也用了sleep。
import java.util.Random;
public class ServeThread extends Thread {
private final Random random;
private final RequestQueue requestQueue;
public ServeThread(RequestQueue requestQueue,String name,long seed){
super(name);
this.requestQueue=requestQueue;
this.random=new Random(seed);
}
@Override
public void run() {
for (int i=0;i<10000;i++) {
Request request=requestQueue.getRequest();
System.out.println(Thread.currentThread().getName() + " handles " + request);
try {
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
}
}
}
}
Main类会首选创建RequestQueue的实例(requestQueue),然后分别创建名为Alice的实例ClientThread和名为Bobby的实例ServerThread,并将requestQueue传给这两个实例,最后执行start。
3141592L和6535897L只是用来作为随机数的种子,没有什么特殊含义。
public class Main {
public static void main(String[] args) {
requestQueue requestQueue=new requestQueue();
new ClientThread(requestQueue,"Alice",3141592L).start();
new ServeThread(requestQueue,"Bobby",6535897L).start();
}
}
Alice requests [RequestNO.0]
Bobby handles [RequestNO.0]
Alice requests [RequestNO.1]
Alice requests [RequestNO.2]
Bobby handles [RequestNO.1]
Bobby handles [RequestNO.2]
Alice requests [RequestNO.3]
Bobby handles [RequestNO.3]
Alice requests [RequestNO.4]
Bobby handles [RequestNO.4]
Alice requests [RequestNO.5]
Alice requests [RequestNO.6]
Bobby handles [RequestNO.5]