【Java】复杂局面线程的同步与互斥 (如何避免服务器不必要的载荷)
情景:
在学习和做项目(C-S框架)的时候遇到这样一个场景:
客户端发出登录请求的时候,服务器接收到请求回应客户端,但是,在网络编程中,服务器接收并处理请求,并将结果返回客户端是需要时间的。如果一个客户端的使用者,频繁点击了登录按钮,那就等同于向服务器频繁发送了n个相同请求,这显然是没必要的,要解决这样产生的服务器负载问题,就必须要从这里的根源出发,去禁止这些多余的无效的请求。
当然,解决方案有很多种:
1.客户端在发送一次有效登录请求后在未收到响应前,禁止再发送登录请求
2.服务器在接收到一次有效登录请求后不再处理该客户端的登录请求
3.当客户端发送一次登录请求后,立马弹出一个模态框,告知用户:正在登录,请稍等。。。 待客户端接收到响应之后再关闭模态框。
这里,我们选择更为人性和合适的第三种方案,在这之前,我们得先了解一下模态框:
所谓的模态框,即弹出后用户只能与对话框交互,而不能与背景页面交互的对话框
在AWT编程中,可在创建Diaglog对象时,指定Modal参数为true,则对话框将具有模态属性
当然,弹出模态框后,其后的代码是不再运行的。
说干就干,首先咱们需要一个自己样式的模态框
这个模态框继承了JDialog,毋庸置疑
各个样式属性不多说了
public class MecDialog extends JDialog {
private static final long serialVersionUID = 2309852253785194778L;
private static final String TITLE = "温馨提示";
private static final Color topicColor = new Color(0, 0, 0);
private static final Font normalFont = new Font("宋体", Font.PLAIN, 16);
private static final Color backcolor = new Color(0x88, 0x88, 0x88);
private static final int PADDING = 15;
private Container container;
// 模态框是否已经弹出
private volatile boolean getByShow;
// 构造方法
public MecDialog(Frame owner, boolean modal) {
super(owner, modal);
getByShow = false; // 初始 未弹出
container = getContentPane(); // 获得“画布”
setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); // 设置右上角退出键无用
setUndecorated(true); // 去除边框
}
MecDialog(Dialog owner, boolean modal) {
super(owner, modal);
container = getContentPane();
setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
setUndecorated(true);
}
public boolean isGetByShow() {
return getByShow;
}
public void setGetByShow(boolean getByShow) {
this.getByShow = getByShow;
}
/**
* 初始化自己的模态框
* 设置其样式
* @param message 需要显示给用户的信息
* @return
*/
MecDialog initDialog(String message) {
JPanel jpnlBackground = new JPanel(new BorderLayout());
container.add(jpnlBackground);
TitledBorder ttbdDialog = new TitledBorder(TITLE);
ttbdDialog.setTitleColor(topicColor);
ttbdDialog.setTitleFont(normalFont);
ttbdDialog.setTitlePosition(TitledBorder.TOP);
ttbdDialog.setTitleJustification(TitledBorder.CENTER);
jpnlBackground.setBorder(ttbdDialog);
jpnlBackground.setBackground(backcolor);
JLabel jlblMessage = new JLabel(message, JLabel.CENTER);
jlblMessage.setFont(normalFont);
jlblMessage.setForeground(topicColor);
jlblMessage.setSize(message.length() * normalFont.getSize(),
normalFont.getSize() + 4);
jpnlBackground.add(jlblMessage, BorderLayout.CENTER);
int height = 5 * PADDING + jlblMessage.getHeight();
int width = 10 * normalFont.getSize() + jlblMessage.getWidth();
setSize(width, height);
jpnlBackground.setSize(width, height);
setLocationRelativeTo(null);
return this;
}
// 显示模态框
void showDialog() {
setVisible(true);
}
void closeDialog() {
dispose();
}
}
运行起来是这个样子的:
在客户端收到响应前,用户无法再点击模态框下的登录按钮,也就禁止了在服务器忙碌时频繁向服务器发送登录请求。
但一切都没想的那么简单。
应用实例:
为了更好的描述问题,不得不简要讲下完成的C-S框架,日后,就此框架,会完成我的相应博客
目前框架分为服务器和客户端,采用TCP长连接方式
简而来说:每当【服务器侦听线程】侦听到有客户端连接,则就会新建线程,专门处理该客户端和服务器的“对话”,这个应用场景下,客户端会发送请求给服务器,服务器处理请求并将结果发回,至于生成及解析消息,分发处理这个场景下暂且不关心。
咱们的模态框就要在此时使用。当然,模态框的弹出会阻塞当前线程,为保证模块框后面的代码顺利执行,模态框要单独是一个线程。
同时为方便处理多个不同的请求,我们需要把模态框存储到一个map中,接收到不同响应关闭不同的模态框
框架内发送请求代码
public void sendRequest(String request, String response, String parameter,
RootPaneContainer parentView, String message) {
if (parentView instanceof Frame) {
new WaittingDialog(
new MecDialog((Frame) parentView, true)
.initDialog(message), response);
} else if (parentView instanceof JDialog) {
new WaittingDialog(
new MecDialog((JDialog) parentView, true)
.initDialog(message), response);
}
sendRequest(request, response, parameter);
}
由以上代码,在每次发送请求前都会new出一个模态框来
btnLogin.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 获得输入的内容
String userId = txtname.getText().trim();
String password = String.valueOf(
new String(pswPassword.getPassword())
.hashCode());
// 生成参数
String args = new ArgumentsMaker()
.addArgument("id", userId)
.addArgument("password", password)
.toGson();
// 发送请求
client.sendRequest("studentLogin", "afterLogin",
args, jfrmViewLoginFrame, "等待登录,请耐心地等待……");
// 为方便测试,在这里给登录按钮多加了一个请求事件
client.sendRequest("getSubjectList",
new ArgumentsMaker()
.addArgument("id", "123")
.toGson(),
jfrmViewLoginFrame, "获取科目信息……");
}
});
这是登陆按钮的点击事件。
下面是一步步的采坑和思考:更改代码也都是在以下的代码中更改
public class WaittingDialog implements Runnable {
public WaittingDialog(MecDialog dialog, String response) {
// 构造等待窗口时(模态框),将其加入客户端会话层的模态框map中
ClientConversation.putDialogLock(response, dialog);
// 这里生成一个新线程,并使其进入就绪态
// 线程名字包含其响应的名字,方便到时候的识别,关闭相应模态框
new Thread(this, "WD-" + response).start();
}
@Override
public void run() {
// 线程显示模态框
dialog.showDialog();
}
}
下面是客户端会话层收到响应时做的操作
// 获取“信息”包含的请求名(action) 和 参数
String action = message.getAction();
String parameter = message.getMessage();
// 通过请求名从map中获得其模态框
MecDialog dialog = dialogMap.get(action);
// 关闭模态框, 从map中移除
dialog.closeDialog();
dialogMap.remove(action);
// 客户端处理响应
client.getAction().dealResponse(action, parameter);
确实,乍一看,没有问题,发送请求前启动模态框显示线程,收到响应后关闭,但实际上验证中,这里存在大问题。
》》》》》》》》》》》》》》线程start只是进入就绪态,并未直接执行《《《《《《《《《《《《《《《《《
倘若,模态框显示线程的代码还没有执行,客户端就已经收到了响应,关闭了模态框,这个时候,显示线程的代码才开始执行,得了,模态框您就显示着吧,关不了了。。。
所以,代码要继续修改
public void run() {
// 通过线程名中带有的响应名,在map中寻找,找不到证明他已经关了
// 这个时候就不需我们再显示模态框了
String response = Thread.currentThread().getName().substring(3);
MecDialog dialog = null;
dialog = ClientConversation.getDialogLock(response);
if (dialog == null) { // 若 那边先close掉,这里就null了
return; // 依然有风险
} // 若那边先close了,又未移除,show就会一直存在
if (dialog != null) {
dialog.showDialog();
}
}
但同样,如代码中注释所说;
若客户端收到响应,刚关闭模态框,还没将action从map中移除,时间片段到了,轮到模态框代码了,他给显示了,就再也无法关掉了
那该如何是好?
@Override
public void run() {
String response = Thread.currentThread().getName().substring(3);
MecDialog dialog = null;
// 改进方式,加入一个锁
synchronized (ClientConversation.class) {
dialog = ClientConversation.getDialogLock(response);
if (dialog == null) {
return;
}
}
if (dialog != null) {
dialog.showDialog();
}
}
String action = message.getAction();
String parameter = message.getMessage();
synchronized (ClientConversation.class) {
MecDialog dialog = dialogMap.get(action);
dialog.closeDialog();
dialogMap.remove(action);
}
client.getAction().dealResponse(action, parameter);
加入锁以后
若客户端会话层先得到锁:
客户端接受响应,关闭模态框,从map中移除模态框不执行完无法进行模态框那边的判断
若模态框线程先得到锁:
他就先会判断请求是否在map中,恩,在,没有关闭,那我显示。但,倘若:模态框刚归还锁,还没来得急显示模态框,这个时候轮到客户端会话层操作,他关闭了模态框,待时间片又轮转到模态框,他显示了,又无法关闭了
可能有人会觉得,把模态框线程那个show也放在锁内不就行了,但是这样模态框线程无法归还锁,因为show模态框真正的代码并不由我们控制。
继续改进:
增加模态框是否已经显示了的属性 getByShow
当先执行客户端对话层代码的时候 getByShow为false,表明在模态框还没显示之前已经收到服务器的响应了,这个时候直接关闭并从map中移除模态框就好
当先执行模态框线程代码的时候,会设置getByShow为true后再归还锁,这个时候轮到客户端对话层执行代码,会再其中等待模态框真正显示出来(isAlive),再进行关闭模态框操作,这样就保证了线程的安全,不会再发生那种显示了却无法关闭的问题。
@Override
public void run() {
String response = Thread.currentThread().getName().substring(3);
MecDialog dialog = null;
synchronized (ClientConversation.class) {
dialog = ClientConversation.getDialogLock(response);
if (dialog == null) {
return;
}
dialog.setGetByShow(true);
}
if (dialog != null) {
dialog.showDialog();
}
}
String action = message.getAction();
String parameter = message.getMessage();
synchronized (ClientConversation.class) {
MecDialog dialog = dialogMap.get(action);
if (dialog.isGetByShow()) {
boolean isActive = false;
while (!isActive) {
isActive = dialog.isActive();
}
}
dialog.closeDialog();
dialogMap.remove(action);
}
client.getAction().dealResponse(action, parameter);
以上代码就是最佳解决方案了↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
总结:
为解决这个问题之前也是试过不少方法,包括用wait,notify去处理这个问题啊,怎么做都会出现一些疏忽的地方。
遇到这样问题的时候,要从多个方向多种情况去考虑问题,不断去优化代码解决问题,最好一行一行一条命令一条命令的去分情况,毕竟计算机是以轮转时间片的方式来进行多任务的
就以上代码来说,至少明白,new出的新线程start方法只是将他添加到就绪态中,并不是立马执行的,他还是会和其他线程去抢占CPU资源,时间片段到了的时候同样也会暂停,一步步分析总能解决。而这种思想,处理线程安全的多情况分析的思想尤为重要。借用尊师的一句话:活人岂能被尿憋死。hhh