场景
当家庭主妇在厨房炒菜时,门铃响了,快递员送来快递,由于炒菜你不能马上去开门,而是让快递员在门口等一会儿,才去打开门。
guarded 保护的意思
Suspension 暂停延迟的意思
下面我们来模拟一下这个场景,快递员的职责是发出收快递的请求,家庭主妇的职责主要是接收请求并处理
代码实现
我们先来实现请求类,比如快递员的请求是接收快递,快递员可能来自顺丰,可能来自圆通,可能来自京东,家庭主妇可能在不同平台买了多种商品,分别使用这些快递
<code>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 + " ]"; } }/<code>
如果同时有多个快递员同时来家里送快递,此时家庭主妇可能让他们都等一下,因此我们需要一个存放请求的类,把快递员的请求都保存起来,然后依次处理
<code> import java.util.LinkedList; import java.util.Queue; public class RequestCollection { private final Queue queue = new LinkedList(); public synchronized Request getRequest() { while (queue.peek() == null) { try { wait(); } catch (InterruptedException e) { } } return queue.remove(); } public synchronized void putRequest(Request request) { queue.offer(request); notifyAll(); } } /<code>
说明:
Queue:队列,先进先出,LinkedList实现了Queue接口
peek方法:如果有元素就返回头元素,如果没有,返回null;该方法不会移除元素
remove方法:如果有元素就返回头元素,否则抛出异常
offer方法:添加元素到队列尾
getRequest方法:如果队列中没有请求,就wait;如果有就获取队列头元素;peek方法会判断队列中是否有请求,如果没有就进入wait,peek方法是保护条件,如果不满足则等待
等待的对象是:请求队列中是否有请求,等待请求队列是否发生变化,导致其发生变化的是将请求放入队列中,将请求从队列中取出
请求的结构:
while(守护条件的逻辑非){
使用wait进行等待
}
执行后续处理
注意这里使用while循环,思考下是否可以使用if
如果使用if,也会进入到wait等待,问题在于如果在wait被打破进入后续处理之前,其他线程将请求处理完的话,问题就出现了,所以这里要使用while
putRequest方法:将请求放入队列的尾部;
快递员发出请求
<code> import java.util.Random; public class Courier extends Thread{ private final Random random; private final RequestCollection requestCollection; public Courier(RequestCollection requestCollection, String name, long seed) { super(name); this.requestCollection = requestCollection; this.random = new Random(seed); } public void run() { for (int i = 0; i < 10000; i++) { Request request = new Request("请求编号: " + i); System.out.println(Thread.currentThread().getName() + " 发出请求 " + request); requestCollection.putRequest(request); try { Thread.sleep(random.nextInt(1000)); } catch (InterruptedException e) { } } } }/<code>
处理请求的家庭主妇类
<code> import java.util.Random; public class Mother extends Thread{ private final Random random; private final RequestCollection requestCollection; public Mother(RequestCollection requestCollection, String name, long seed) { super(name); this.requestCollection = requestCollection; this.random = new Random(seed); } public void run() { for (int i = 0; i < 10000; i++) { Request request = requestCollection.getRequest(); System.out.println(Thread.currentThread().getName() + " 处理请求 " + request); try { Thread.sleep(random.nextInt(1000)); } catch (InterruptedException e) { } } } }/<code>
主函数类
<code> public class Main { public static void main(String[] args) { RequestQueue requestQueue = new RequestQueue(); new ClientThread(requestQueue, "Alice", 3141592L).start(); new ServerThread(requestQueue, "Bobby", 6535897L).start(); } }/<code>
模式解读
1、和临界区模式的区别
两种模式都使用synchronized,但是GuardedSuspension添加了判断条件
2、注意wait和notify的使用
如果没有notify,那么会一直等待。
可以设置wait超时
3、可以不使用wait
这样的话一直执行while循环,此时可以使用yield方法让出执行时间
while(逻辑判断非){
Thread.yield();
}
这叫busy wait
LinekdBlockingQueue
下面我们使用LinkedBlockingQueue来改造上面的程序
修改RequestCollection类
<code> import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class RequestCollection { private final BlockingQueue queue = new LinkedBlockingQueue(); public Request getRequest() { Request req = null; try { req = queue.take(); } catch (InterruptedException e) { } return req; } public void putRequest(Request request) { try { queue.put(request); } catch (InterruptedException e) { } } } /<code>
说明:
1、上面的take和put方法做了互斥处理,因此这里不需要使用synchronized关键字
问题:
将RequestQueue中的putRequest方法中两条语句的顺序颠倒过来,即如下:
<code> public synchronized void putRequest(Request request) { notifyAll(); queue.offer(request); }/<code>
先运行notifyAll,程序是否正确?
答案:正确,因为调用notifyAll后,线程仍旧持有锁,其他线程虽然被唤醒,但是马上会阻塞(不会做条件判断),只有在该方法调用后,线程释放锁,其他线程才有机会执行。
但是最好将notifyAll写在该方法的最后,因为就是比较容易理解。
问题:
对于getRequest方法
<code> public Request getRequest() { while (queue.peek() == null) { try { synchronized(this) {} wait(); } } catch (InterruptedException e) { } } return queue.remove(); }/<code>
如果将锁加在while循环代码体上,会不会出问题?
答案:
会出问题,原因:如果有两个线程都同时wait,之后被唤醒,如果此时只有一个请求,那么queue.remove会出错,因为一个线程已经取出数据,另一个就没法取出数据了。
问题:
对于getRequest方法将try/catch放在while循环外部,程序会不会出问题?
答案:会出问题,因为wait方法如果被打断,那么就会执行queue.remove方法,此时不会再调用queue.peek方法做见检查,如果此时queue中没有请求,就会出错。
问题:
如果将getRequest方法中的wait改为Thread.sleep方法,程序是否有问题?
答案:会有问题,原因是:sleep方法不会是释放锁,wait会释放实例对象的锁。由于sleep不释放锁,那么线程将会一直持有锁,而其他线程就无法放入request,那么queue.peek就会一直为null,此时程序整个就没有任何活动,这种情况称为活锁。