Guard是被守护被保卫的意思,我们可以将GuardSuspension模式看作是singgleThreadExcution模式和一个判断条件所组成的,这么叙述太笼统,可以结合下面的案例来体会一下这个过程:
我们模拟的是客户端向服务器端发送请求,服务器端做出反馈的例子,模拟的线程之间最简单的通信:
1.构建请求类:
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+" ]";
}
}
其中使用到了上面讲述的immutable模式,这样可以保证name字段在不同的线程之间的唯一性。当有了请求类之后我们会想到请求会通过客户端不断地产生,假如服务器端的处理能力是有限的,那么怎么解决呢?我们可以设计一个队列,用于存放处于等待处理的request:
请求队列类:
public class RequestQueue {
private final Queue<Request> queue = new LinkedList<Request>();
public synchronized Request getRequest(){
while(queue.peek() == null){
try{
wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
return queue.remove();
}
public synchronized void putRequest(Request request){
queue.offer(request);
notifyAll();
}
}
请求队列中声明了一个链式列表用于储存等待的request类,并设计了getRequest()方法和putRequest()方法,并使用synchronized修饰,从而保证了对共享的实例操作的唯一性。其中在这个类中就是用到了这节课所说的设计模式,其中while后面的语句用于判断,意思就是如果等待队列中没有请求信息时,需要等待,,直到这里面有请求之后才弹出队列中的请求信息,传递给服务器端,我们把while后面的语句称作为守护条件,也就是说在原有的SigngleThreadExecuton模式中增加了一个守护条件,以保证业务逻辑和线程之间共享资源的安全。这样,reques类和RquestQueue类都是线程安全类。然后设计客户端,用来不断的发送信息:
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);
}
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){
e.printStackTrace();
}
}
}
}
客户端类继承了Thread类,即在这个类中重写run()方法后执行新的线程来进行发送信息的任务,,其中run()方法中仅仅包含了一个循环体,循环体中循环产生request,同时将它放入requestqueue等待队列中,设计一个sleep()函数来模拟耗费时间。这个类构建时需要一个RequsetQueue类的实例,所以当我们传入这个实例时要保证这个类是线程安全的,我们设计的RequestQueue类符合这个要求。
同时设计一个服务器端,用来处理消息:
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);
}
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){
e.printStackTrace();
}
}
}
}
与客户端相同,我们也需要保证传入的等待队列是线程安全的,所以我们上面设计的RequestQueue类比较关键,起到了一个连接服务器端和客户端的通道的作用,而request请求则是通道中的传递物,服务器端同时启用一个新的线程来将星系打印出来,这同样需要继承Thread类。随后通过一个main函数来启动这个过程:
public class TestMain {
public static void main(String[] args) {
RequestQueue requestQueue = new RequestQueue();
new ClientThread(requestQueue,"Alice",3145192L).start();
new ServerThread(requestQueue,"Bobby",6535897L).start();
}
}
运行结果如下;
当然我们也可以使用java.util.concurrent.LinkedBlockingQueue来重新设计RequestQueue类:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class RequestQueues {
private final BlockingQueue<Request> queue = new LinkedBlockingQueue<Request>();
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){
}
}
}
其中java.util.concurrent.LinkedBlockingQueue类的作用和上面设计的RequestQueue的作用是相同的,因为其实现了BlockingQueue接口,并且take和put方法都考虑了互斥处理,所以也就无需声明方法为synchronized修饰。
同时可以修改下原来设计的RequestQueue类,了解一下wait()和nitfyAll方法的运行,是否符合之前所说的那样:
public class RequestQueue {
private final Queue<Request> queue = new LinkedList<Request>();
public synchronized Request getRequest(){
while(queue.peek() == null){
try{
System.out.println(Thread.currentThread().getName() + ": wait() begins, queue = " + queue);
wait();
System.out.println(Thread.currentThread().getName() + ": wait() ends, queue = "+ queue);
}catch(InterruptedException e){
e.printStackTrace();
}
}
return queue.remove();
}
public synchronized void putRequest(Request request){
queue.offer(request);
System.out.println(Thread.currentThread().getName()+": notifyAll() begins, queue = "+queue);
notifyAll();
System.out.println(Thread.currentThread().getName()+": notifyAll() ends, queue = "+queue);
}
}
执行结果如下:
针对下面的代码,考虑一下是否会出现问题?
public synchronized Request getRequest(){
while(queue.peek() == null){
try{
System.out.println(Thread.currentThread().getName() + ": wait() begins, queue = " + queue);
wait();
System.out.println(Thread.currentThread().getName() + ": wait() ends, queue = "+ queue);
}catch(InterruptedException e){
e.printStackTrace();
}
}
return queue.remove();
}
public synchronized void putRequest(Request request){
queue.offer(request);
System.out.println(Thread.currentThread().getName()+": notifyAll() begins, queue = "+queue);
notifyAll();
System.out.println(Thread.currentThread().getName()+": notifyAll() ends, queue = "+queue);
}
(1)将getRequest方法中的while改为if:
在本例中是不会发生问题的,但是在一般情况下还是存在着问题,假设多个线程都在wait()时,RequestQueue的实力被执行了notifyAll。这样以来多个线程就会同时运行,这时如果等待队列中只有一个request,第一个线程调用了queue.remove()之后,等待队列会变为空,queue.peek()的值变为null;但是第二个线程级之后开始的线程已请确认了之前的守护条件成立,那么即使是守护线程成立queue.remove()还是会被调用,因此正在wait()的线程在开始运行之前一定要检查守护条件,所以不应该使用if而是使用while。
(2)将synchronized修饰范围改为只修饰wait()方法。
一般情况下还是会发生问题的,因为当程序运行到synchronized代码块外面时会检查条件和调用remove方法,当queue队列中仅有一个元素时,线程可能会抛出NoSuchElementException异常,同时,LinkedList本来就是非线程安全的,程序会欠缺安全性。
(3)将while守护条件饭知道try...catch里面。
假设当线程正在wait()时,其他线程调用了Interrupt()方法,即时守护条件不成立,该线程也会跳出while循环语句,进入catch代码块,调用remove方法,也就是说,“等到守护条件满足”这一功能并没有实现。
(4)将wait()替换为Thread.Sleep
由于wait和Thread.Sleep()不同,直线wait()线程会释放对象实例的锁,而sleep()方法则不会,因此如果被synchronized修饰的getRequest方法中执行sleep()方法的话,那么其他线程,无论哪一个都无法进入putRequest方法或者getRequest方法,即已经陷入阻塞状态。由于无法进入putRequest方法,所以守护条件永远都不会成立,所以会重复的进行醒来-->检查-->休眠-->醒来的状态,线程一直处于阻塞状态。