【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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值