简单的并发编程中犯2的一个小例子--CAS使用时必定要考虑下是否有必要作轮询

场景:java

在一个app中,我须要为访问者提供某种信息的存储,因为架构上已经肯定的方式,因此能够确保每个app上存储的用户不会太多,因而就放在了内存中,而不是缓存。mysql

 

这些信息须要按期清理掉,就像会话同样,每一个用户都会有一个惟一的key标识符,用一个ConcurrentHashMap存放,长时间不使用就须要删除掉了。ajax

 

可是它与会话不一样的是,在清空的同时会清空掉许多用户级别的网络通讯对象,例如Socket或数据库链接对象等。所以它的清理将与传统的清理方法有一些区别,为什么?sql

 

由于当清理程序发现须要清理该对象的时候,这个对象正好被一个有效请求所使用,在清理对象的时候,须要将内部的Socket等资源关闭,就会致使问题。数据库

 

所以我不得不在这个用户级别的对象上去作一个状态:浏览器

简单来讲有一个FREE、USE、DELETE三种状态,FREE是能够修改成任意状态的,USE是使用状态的,DELETE是删除状态的。USE状态的不能被删除,DELETE状态的不能再被使用。缓存

简单逻辑是:服务器

一、若是经过ConcurrentHashMap获取到相应的对象后,须要断定状态不能是DELETE,再尝试在对象上修改状态为USE才能使用,若是修改失败则不能被使用,固然是用后会更新下最新的时间,这个时间将用volatile来保证可见性,以便于最近不会被清理掉,使用完后会讲对象的状态从新修改成FREE。伪代码以下所示:网络

 

[java] view plain copy架构

在CODE上查看代码片派生到个人代码片

  1. int old = status.get();  
  2. if(old != DELETE && status.compareAndSet(old , USED)) {  
  3.      return this.userXXXDO;  
  4. }  
  5. return null;  


二、在删除操做前也必须先获经过ConcurrentHashMap取到对象,须要断定状态不能是USE,而后尝试将状态修改成DELELE才能真正开始作删除操做。代码与上面相似。

 

 

这个逻辑彷佛看似完美,我当时晕头转向的也认为CAS就能够简单搞定这个问题,作几个状态嘛,简单事情,呵呵。

结果之外发生了,外部程序偶然状况下获取不到这个对象,可是在获取不到这个对象的断点中,我使用表达式再执行一次又能获取到,这尼玛是什么问题发生了呢?

 

刚开始我也跑偏了,由于外层有一个ConcurrentHashMap,思惟凝固在是否是这有并发可见性问题,不过这样的猜想连我本身都没有相信,由于我对这个组件的内在的源码是比较了解的,若是它有问题,就完全颠覆可见性的问题了。

 

在不断加班到半夜的迷糊中,迷迷糊糊地跟踪代码,发现里头还有一层,就看到点但愿,看到了刚才的代码。咋一看,代码没有啥问题,由于这个就是状态转换,并且这个是在一个用户下的操做,一个用户并发的几率原本就很低,并且有CAS来保证原子性,能有什么问题呢?

 

后来一个哥们问我可不能够用synchronzied一会儿提醒了我,个人第一反应是不到万不得已不用这个,这个若是放在内部作就是全部的状态转换所有要加上,悲观锁就很差了,放在外面更不靠谱,那就是一个全局的ConcurrentHashMap,那用它来控制个毛的并发啊,我就是要把锁打散。

可是这个提示让我在迷迷糊糊中醒了一下,我发现可能真的有并发问题,或者说假设一个用户的客户端同时发送多个请求上来,此时因为是同一个用户的请求是同一个,因此KEY确定是同样的,缓存用户对象也应该是同样的,此时若是两个请求都运行到代码:

int old = status.get();

那么两个请求在此时获取到的状态值就是同样的,当发生CAS的时候,只有一个会成功,另外一个不成功的就返回null了,代码看了好久,虽然很简单,可是只可能这里有问题。

 

考虑实际场景,还真的可能有一个客户端的浏览器同时发起多个请求的状况,由于客户端并非简单的页面跳转(简单页面用户手点击再快也有时差),而是与服务器端不少ajax交互,当一个选项发生变化的时候,确实有可能同时发起多个ajax请求。

 

不过怎么改呢?用syncrhonezized,显示我不是那么容易放弃本身的人,哈哈,迷迷糊糊中终于才想起来,CAS也须要考虑下尝试,确实是这样,那么就改成循环来作。

可是一旦改成循环大伙第一个担忧的问题就是可否退出循环,Java的里面有许多死循环方式,可是这种代码不退出就是一个大问题,可是限制次数的话,多少为好?这很差说,由于乐观锁在这个阶段是很差讲清楚具体的次数的,或许在许多人眼中这算是小问题,可是我认为在这些问题上是关键的关键,若是不注重就会出大问题。

后来考虑来考虑去发现这样写没问题:

 

[java] view plain copy

在CODE上查看代码片派生到个人代码片

  1. int old = status.get();  
  2. while(old != DELETED) {  
  3.     if(old == USED && <span style="font-family: Arial, Helvetica, sans-serif;">status.compareAndSet(old , USED)) {</span>  
  4.         return this.loginDO;  
  5.     }  
  6.     old = use.get();  
  7. }  
  8. return null;  

 

 

这个while循环的条件是状态没有被删除,状态只要有被删除,这个请求就应该有机会去获取使用机会,只要有机会就应该去尝试,你们会想会不会一直不成功呢?那不会,乐观锁的道理就是咱们足够乐观,由于咱们发生到这个点上的问题都是偶然,并且是用户级内部发生,因此它尝试的几率很是低,在这样作的方式下,咱们采用乐观机制避开了悲观锁带来的巨大开销,同时又能保证原子性。

而对于删除就没有必要循环了,删除操做发现状态是USE就不能删除,状态为FREE在作CAS的时候若是CAS征用失败也没有必要再去征用,为什么?假若有两个线程在征用DELETE,另外一个成功了就OK了,若是有一个USE在与之征用,它自己就没有再征用的必要。

 

到这里问题基本解决,可是这个程序是否是就没有问题了呢?

未必然也,由于最初咱们写代码的时候没有考虑到多个请求同时发起的过程,因此也天然不会考虑到多个请求将状态改成FREE的过程,假若有2个请求,其中1个请求释放掉了将状态修改成FREE,而另外一个还在使用中,此时有线程想将它DELETE掉,发现FREE状态,是能够删除的,因而将相应的Socket关闭掉,就出大事了。

 

若是要彻底解决这种问题,还须要一个条件变量来使用和释放的次数,使用时加1,释放时候减掉1,这就有点像Lock机制了,只是可控性上更强,可是对于代码复杂性更大,你本身也须要承担更大的责任。

 

若是在应用中,出现这种问题的几率极低,那么能够暂时用状态也能够,或者为了简单处理也能够直接换成Lock。为什么说几率低呢?由于这种数据的清理理论上不会到秒级别,例如10分钟,一个请求来的时候,会刷新最近的操做时间,后台操做即便一长一短,只要误差不是10分钟以上,在理论上就不会有问题。

 

你们可能一想,通常要求系统响应3s,不会有那种状况发生。真的是这样嘛?我认为未必,所谓3s只是常规系统,有的系统就未必了,例如WEB版本的数据库软件,经过UI上输入SQL获取结果,WEB版本的安装系统给上千的服务器安装相应的软件等等,这些操做的响应都是能够很长的,这个值是有可能超过咱们的清理时间的,因此一切皆有可能,当你真正遇到的时候,但愿这些小思路能帮助到你。

相关文章
相关标签/搜索