并发-cas解析

48 阅读3分钟

cas这个想必大家都不陌生,全名叫Compare And Swap,比较并交换,也可以理解是java的一个轻量级加锁的解决方案。

原理

从名字也能看出来他的基本原理,就是每次加锁前,会先将主内存的锁对象对应的值,刷新的工作内存中,等执行完毕后,比较工作内存的中的锁的值和主内存中锁的值是否一致,若一致则认为锁对象没有发生变化,将修改提交,刷新到主内存中。若不一致,则重新获取一遍主内存的值,重新执行对锁对象的操作,再次提交去比较两者的值。

讲述了一边原理后,其实就能发现一个最最经典的问题,也就是aba问题,cas判断锁对象是否修改过,是根据值来判断的,这里就会有一个问题,如果a,b两个线程同时去竞争锁,a先执行完,对锁对象,进行了操作,但是最后又将锁对象还原,这是b后执行完发现,锁对象的值没有发生变化,也会认为这个锁对象是跟他获取的锁对象是一致的,也会执行成功,但实际上来说这个锁对象已经被操作过了。

可能有人会说这里有什么危害呢?本身我也是要执行的,只要保证值一样,锁对象被操作过,也没什么问题。

这里一个转账问题,就能看出aba的危害了。

假设A给B再ATM1上转账100,因为某种原因阻塞了。 A很急于是去ATM2上给B转账100,成功了。 这时候正好C想用A的卡消费,发现余额不足了,就给A冲了100。 然后,ATM1的线程阻塞结束了,开始执行转账操作。发现锁的值,没有发生改变,于是认为锁没有发生过变化,又将100转给了B。最好的情况当然是业务上A执行了两次操作,所以转了两次B。当然从技术角度看没什么问题,用户调用两次,执行了两次。但是从业务来看,这里两次转账其实应该是一次的操作。所以很多时候还是要从设计上避免aba的问题。

如何避免其实也很简单,就说可以在对象上添加版本号,或者时间戳。两者都相同,则认为这个锁对象是一个没有操作过的锁对象。

总结

cas是一个轻量级加锁的方案,但是使用的时候还是要注意两个方面一个是aba问题,一个是自旋的问题,aba问题其实更多是业务场景上的问题,可能会导致某些业务上需要只执行一次的情况变为执行了多次,解决的方案也比较简单就说在锁对象上加上时间戳或者版本号java也提供了AtomicStampedReference等原子类来实现带时间戳的cas加锁方案。自旋问题,是一个性能层面上的问题,由于cas是个乐观锁,它会认为自己一定能拿到锁,所以线程会不停的等待直至获取到锁,但线程并发多且线程执行时间较长的时候,会导致效率低下,资源占用严重。所以可以在使用cas进行锁控制的时候,加上等待时间或者执行次数,保证线程不会长时间处于阻塞挂起的状态。