SSH框架中的并发编程控制

2019-04-13 14:08发布

在网络版的预付费售电管理系统和农排售电管理系统在现场长期运行的过程中,出现了以下几个问题:1、购电记录重复(相同购电次数有多条购电记录存库,购电金额等数据有可能完全一样,也可能都不一样,但共同点是入库时间很接近);2、同一块儿电表被分配给了多张电卡或者同一个电卡开户给了多个用户。
在分析、解决现场问题的过程中逐渐发现:问题的出现是因为我们忽略了多线程并发编程中对共享资源的同步问题。
什么时候必须同步?什么叫同步?如何同步?
要跨线程维护正确的可见性,只要在几个线程之间共享非 final 变量,就必须使用 synchronized(或 volatile)以确保一个线程可以看见另一个线程做的更改。
为了在线程之间进行可靠的通信,也为了互斥访问,同步是必须的。这归因于java语言规范的内存模型,它规定了:一个线程所做的变化何时以及如何变成对其它线程可见。
因为多线程将异步行为引进程序,所以在需要同步时,必须有一种方法强制进行。例如:如果2个线程想要通信并且要共享一个复杂的数据结构,如链表,此时需要确保它们互不冲突,也就是必须阻止B线程在A线程读数据的过程中向链表里面写数据(A获得了锁,B必须等A释放了该锁)。
为了达到这个目的,java在一个旧的的进程同步模型——监控器(Monitor)的基础上实现了一个巧妙的方案:监控器是一个控制机制,可以认为是一个很小的、只能容纳一个线程的盒子,一旦一个线程进入监控器,其它的线程必须等待,直到那个线程退出监控为止。通过这种方式,一个监控器可以保证共享资源在同一时刻只可被一个线程使用。这种方式称之为同步。(一旦一个线程进入一个实例的任何同步方法,别的线程将不能进入该同一实例的其它同步方法,但是该实例的非同步方法仍然能够被调用)。
下面以解决“购电记录重复”问题为实例阐述我们在SSH框架中的线程同步解决方案。
针对“购电记录重复”问题,我们在设计之前就想到过这个问题,而且也采取了预防措施:在购电时从界面上传入一个读卡读到的“购电次数”,在存库时与数据库中存储的“购电次数”进行比较,只有在界面传入的购电次数小于库中存储的“购电次数”时才允许存库。但这个问题依然在出现。原来我们以上的预防措施仅仅适用于不并发执行任务的场合,对于BS结构中天生的多线程编程模式基本不起作用,仅仅是使这个现象出现的概率变小了、更不容易发现问题了,但并没有完全解决问题。同一个用户在两个客户端执行购电业务或者是同一个客户端重复提交多次存库操作,此时就出现了多个线程对数据库资源IC卡档案中的“购电次数”的共享现象。在第一个线程还未完全提交更改后的IC卡“购电次数”之前,第二个线程如果也执行存库操作,走到“验证购电次数是否合法”这一步,此时取到的IC卡“购电次数”还是更改前的购电次数,因此验证结果合法,最终导致的结果是同一个购电次数入库两条购电记录。如果是更多的线程执行类似的操作,会有更多的重复购电记录。
解决方法找到了,我们需要对共享资源--数据库中存储的IC卡状态、IC卡“购电次数”执行同步处理:在第一个线程未将修改后的IC卡“购电次数”成功提交之前,不允许其他线程读取IC卡“购电次数”以执行“验证点次数是否合法”操作。其他线程需要等待,直到第一个线程成功提交事务,并且释放对象锁。接下来就是在代码中具体解决该问题了。
首先想到的是:在Dao层(ArSalemgrDaoImpl)锁定IC卡对象,例:
public boolean saveDailysale (final DailysaleForm dailysaleForm) {
YcArIccustomer ycArIccard = findArIccard(dailysaleForm.getCardid());
synchronized (ycArIccard) {
。。。。。。
if((ycArIccard.getMtrbuytimes())>(dailysaleForm.getBuytimes())){//如果库里的购电次数与界面传入的购电次数不一致,则不再向下执行
return false;}
。。。。。。
session.save(ycArIccard);
}

}
测试结果:问题未解决。经过断点调试,打印出几次线程取到的购电次数观察,发现该同步措施不起作用,该代码段执行完成之后,并未提交结果入库,后面的线程读到的购电次数依然是修改前的购电次数。我们在整个函数saveDailysale之前加同步限制测试的结果也是如此。
问题出在什么地方呢?在SSH框架中,有spring通过作用于service函数进行事务控制,只有整个service函数执行完成,才会执行存库提交操作。
因此,方案二:我们在service层(ArSalemgrServiceImpl)中对购电时的存库函数执行同步操作,例:
public synchronized boolean saveDailysale(DailysaleForm dailysaleForm){
return arYcSalemgrDao.saveDailysale(dailysaleForm);
}
经过断点调试、观察,在第一个线程执行完saveDailysale函数后,成功地提交了修改后的结果,在数据库中已经看到了修改后的数据。但执行下一个线程的时候,取到的IC卡购电次数依然是修改前的购电次数,因此“购电记录重复”的问题还没有解决。
在Dao层(ArSalemgrDaoImpl)的函数saveDailysale中增加以下的代码段:
session.flush();
session.clear();
问题彻底解决,原来是session缓存的问题导致了后面线程取到的IC卡“购电次数”跟数据库中的结果不一致,只需刷新一下缓存,问题解决了。
以上列举的是现场较多出现的问题,我们的系统中估计还有一些这样类似的因缺少对共享资源同步控制造成的问题,只是问题出现的概率小或者是还没有出现过。以前在编码的时候缺乏经验,我从没有意识到并发同步问题,现在也只是刚开始了解,目前能够想到和说到的只有这些,这只是我的个人理解,有些说法未必正确、有些考虑可能还有不够全面的地方,请大家多提提意见,当是互相交流、互相提高吧。