用户常常有一个习惯,那就是在手机或电脑卡的时候多次点击,我们知道,通常视图界面是一个单另的线程案例,而网络这边是另一个线程,卡是因为我们的服务器端向客户端发送响应速度慢,而这时候视图界面还是运行态,因此用户的多次点击是一种无效而且对服务器增加压力的一种体现。
为什么需要一个进程显示视图,另一个线程做网络传输,这是因为我们不可能发送请求后就令客户端阻塞,而且等待响应回来的时候才唤醒客户端线程,这无疑是对资源的低效利用,在发送后,客户端线程往往还需要做别的操作,这样才是对资源的有效利用。(就是qq你点了个登陆以后变成正在登陆了,你点不了登录了,然后qq后台还在计算你的很多数据。)
而且请求一次并未等到有响应了才会发下一条请求,因此产生了多线程同步的问题。
下图说明了单次请求和响应还有模态框之间线程关系。
模态框可以解决上述问题,本文以登录为案例进行叙述,用户点击按钮后弹出模态框,禁止用户再次点击任何按钮,在响应返回时,也就是客户端在开始处理后台发送的响应时,关闭模态框。问题看似简单,然而在多线程的条件下会出现很多线程同步问题,后面会一一介绍。
首先看一下我们要用的api:JDialog
swing中常见的一个类,在父窗口弹出一个新窗口,令父窗口无法被限定。
特性:阻塞当前线程,也就是在dialog.showDialog();下面的代码是无法执行的,且!
并不会释放锁。也就是说如果你在synchronized 锁中要是调用了这句话,就会造成线程的阻塞并且不释放锁。
这样生产者消费者模式就无法解决该类问题,原因就是弹出模态框会阻塞线程,而这种生产者消费者模式需要在弹出模态框后对信号量进行处理,然而Dialog的阻塞并不释放锁的特性令我们无法进行处理。
那我们来思考一下,模态框的显示和关闭应该是哪里来进行,应当都在客户端,只不过是不同的线程,分析一下有哪些线程,
1.页面线程(swing)线程
2.发送请求的线程
3.客户端处理来自服务器端响应的线程。
显示模态框的线程应该是2,发送请求后跳出模态框阻塞。
让2唤醒的线程应该是3。
1线程始终保持着正常运行。
现在我们让页面异步发送两个请求
基本的数据传送就是这样,我也写了,然而根本不能完成该需求。会出现模态框根本不会消失的错误问题。这很奇怪,一个显示模态框,一个关闭模态框,是不应该出现这种问题的,刚开始想到了JMM的一些特性,加了许多的voliater然而还是不能解决问题,经过不断地尝试和查看内存信息,最终发现了原因。
原因分析:首先说两点,1.网络传输是耗时的且时间浮动很大。2.打开模态框和关闭模态框是两个线程,然而打开模态框的操作并非是能在一个时间片段内完成!看下源码
如此一大段代码是未必能在一个时间片段里完成的,这会出现一个问题,网络速度如果很快,我打开模态框到一半了,上下文切换了,关闭了模态框,上下文再次切换到打开模态框,然后模态框就再也不会消失了。如果再快一点,发送请求后直接客户端接受相应处理线程直接就关闭模态框了,发送请求端才开始执行showDialog语句。快会让我们的模态框无法关闭。
2.发送响应接受请求如果在多线程的环境下,可能出现以下情况:发送了两个请求,返回了两个响应,响应顺序与发送顺序不一定是相同的。
针对以上两种问题做出以下处理。
针对一,新开启线程去显示模态框,这样就不会阻塞我们的线程,但这也增加了线程数量,增大了编程难度。
针对二,我们把response与模态框绑定在一起,根据response来关闭模态框,让我们的程序可以处理请求和相应不对应问题。
在发送请求的线程中开启新的子线程来显示模态框。
public class WaittingDialog implements Runnable {
/**
* 和客户端发送线程是同一个线程,且在客户端发送发送请求之前完成了模态框显示线程的
* @param dialog
* @param response
*/
public WaittingDialog(MecDialog dialog, String response) {
ClientConversation.putDialogLock(response, dialog);
new Thread(this, "WD-" + response).start();
}
@Override
public void run() {
String response = Thread.currentThread().getName().substring(3);
MecDialog dialog = null;
//synchronized令下述的dialog操作具有原子性,因为对dialog的读操作在客户端处理响应线程
//中运用了相同的锁来完成互斥。
synchronized (ClientConversation.class) {
//这是未必能取到的,因为如果服务器端返回过快会删除掉之中的Dialog
dialog = ClientConversation.getDialogLock(response);
if (dialog == null) {
return;
}
dialog.setGetByShow(true);
}
if (dialog != null) {
//阻塞该线程,但Swing线程中的代码还在继续执行
dialog.showDialog();
}
}
}
在我们创建好了模态框和响应的映射的时候加入map,注意在令该线程进入就绪态时请求可能已经发送出去了,更有可能响应已经回来了。
下面是我们在处理服务器响应的线程中需要执行的代码。
public void dealResponse() {
String action = message.getAction();
String parameter = message.getMessage();
//互斥锁
synchronized (ClientConversation.class) {
MecDialog dialog = ClientConversation.dialogMap.get(action);
//利用自旋CAS来等待到模态框已经成为了活动窗口,当模态框显示了以后
//就结束自旋
if (dialog.isGetByShow()) {
boolean isActive = false;
while (!isActive) {
//window的方法javaapi1.6:返回此窗口是否为活动窗口。
//MecDialog enxtends JDialog extends Dialog extends window
isActive = dialog.isActive();
}
}
dialog.closeDialog();
ClientConversation.dialogMap.remove(action);
}
...........处理响应的具体操作
}
我们具体模态框的假类图,volatile保证了线程间的通讯。
对于锁的应用是很重要的,这里运用锁达到了原子性和有序性,简化了编程的逻辑性。
对于客户端发送请求就是放在map中,看map里有没有Dialog,然后发请求,然后根据map里头有没有dialog来显示模态框。
对于客户端处理响应,就是看有没有显示,没有显示就map中删除掉模态框,令客户端发送线程请求就不显示模态框了,有就等待模态框出来再关闭。
逻辑没有问题了,经过不断地测试,确实也是没问题了。对于多线程的编程难度还是很大的。
指导者:微易码科技