好的,这一章我们继续介绍Guarded Suspension模式^_^。
Guarded Suspension模式是在Single Threaded Execution模式基础上,只有满足守护条件的时候,才会执行相应的逻辑,否则当前线程进入对象等待队列。
示例代码角色如下:
- 客户端线程ClientThread:往队列RequestQueue中添加数据;
- 服务端线程ServerThread:从队列RequestQueue中请求数据;
- 队列RequesuQueue:存放请求资源Request;
- Main:测试入口类。
时序图如下所示:
从时序图可以看到,当RequestQueue中存在Request资源时,ServerThread的getRequest请求可以立即执行;而RequestQueue中不存在资源时,ServerThread的getRequest请求将被阻塞,知道ClientThread执行putRequest方法后才能继续执行。(时序图中ClientThraed和ServerThread都被加粗,这是因为它们是主调对象;而RequestQueue是被调对象)
话不多说,上代码:
public Class Request{
private String name;
public Request(String name){
this.name = name;
}
public String getName(){
return this.name;
}
@Override
public String toString(){
return "[Request " + name +" ]";
}
}
public class RequestQueue{
private final Queue<Request> queue = new LinkedList<Request>();
public synchronized Request getRequest(){
while(queue.peek() == null){
try{
wait();
}catch(InterruptedException e){
}
}
return queue.remove();
}
public synchronized void putRequest(Request request){
queue.offer(quest);
notifyAll();
}
}
public class ClientThread extend 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);
requestQueue.putRequest(request);
System.out.println(Thread.currentThread.getName() + " put" + request.toString());
try{
Thread.sleep(random.nextInt(1000));
}catch(InterruptedException e){
}
}
}
}
public class ServerThread extends Thread{
private final Random random;
private final RequestQueue requestQueue;
public ServerThread(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()" + " get" + request.toString());
try{
Thread.sleep(random.nextInt(1000));
}catch(InterruptedException e){
}
}
}
}
public class Main{
public static void main(String[] args){
RequestQueue requestQueue = new RequestQueue();
new ClientThread(requestQueue, "producerA",3141592L).start();
new ServerThread(requestQueue, "consumerA",6543872L).start();
}
}
例子中的守护条件是queue.peek()!=null,while内部的条件是守护条件的逻辑非。当守护条件不满足时,while之后的语句也不会执行。当某个线程执行wait方法,当前线程需持有当前对象的锁;当前对象执行notifyAll之后,等待队列中的线程退出等待,能否获取CPU资源还需要进一步持有对象锁。这里需要注意的是,守护条件需要由while执行,而不能由if执行,因为使用while时,等待的线程往下执行的时候,会再次判断守护条件是否满足。
- guardedMethod:由while和wait实现;
- stateChangingMethod:由notifyAll实现
注意到Guarded Suspension模式使用了Synchronized关键字,使用线程同步的方式。那么为什么需要同步呢?因为需要避免对象状态的不一致。下述例子就是一个简单的不一致:
LinkedList为线程不安全的类,而LinkedBlockQueue为线程安全的类,UML图如下:
实现方式如下:
public class RequestQueue{
private final Queue<Request> requestQueue = new LinkedBlockQueue<Request>();
public Request getRequest(){
try{
Request request = requestQueue.take();
return request;
}catch(InterruptedException){
}
}
public void putRequest(Request request){
try{
requestQueue.put(request);
}catch(InterruptedException){
}
}
}
Guarded Suspension模式包含Guarded Object(被守护的对象)。Guarded Object持有被守护的方法(Guarded Method)。当线程执行Guarded Method时,若守护条件成立,则可以立即执行;若守护条件不成立,则需要等待。同时Guarded Object中还需要有改变示例状态(守护条件)的方法(stateChangingMethod),否则线程可能一直处于等待队列中。
Tips:
1.先执行notifyAll方法,再执行queue.offer(request)也是线程安全的,因为执行notifyAll之后,当前线程不会释放锁,因此等待队列中的线程也无法获得锁(也不会执行检查守护条件)。在执行offer方法之后,当前线程释放锁,阻塞线程之一获得锁,再继续执行(先检查守护条件);
2.while如果改为if,因为执行notifyAll会唤醒所有的等待进程,如果queue中只有一个request被获得锁的进程消费之后,其他进程不会再检查守护条件,从而造成程序不安全;
3.如果将synchronized只包含wait,那么while条件判断和remove可能会并发执行,从而造成不一致性;如下所示:
public Request getRequest(){
while(queue.peek() == null){
try{
synchronized (this){
wait();
}
}catch(InterruptedException e){
}
}
return queue.remove();
}
线程的执行顺序可能如下,从而造成程序异常:
4.将try,catch挪到while的外面,如果当前等待的线程,其他线程调用了interrupt方法,则会直接到catch方法,不会再判断守护条件;
5.将wait替换为Thread.sleep,程序生存性问题会存在。如下所示:
public synchronized Request getRequest(){
while(queue.peek() == null){
try{
Thread.sleep(100);
}catch(InterruptedException e){
}
}
return queue.remove();
}
可能有同学会想到每隔100ms才去检查守护条件可能造成性能下降,当时这样写却能造成程序生存性的问题。因为sleep方法不会释放对象锁,而wait方法会释放对象锁。因此某个进程在getRequest中执行了sleep,那么其他进程会一直阻塞,setRequest方法也无法执行。类似于死循环,称之为活锁。线程在醒来->检查条件->休眠的过程中无限循环。
好啦,关于Guarded Suspension设计模式我们就介绍这么多,下一章将介绍Balking模式,欢迎大家继续阅读。