CAS(compare and swap)
CAS(Compare and Swap)是一种乐观锁机制,是一种原子操作(硬件层面让比较和赋值这俩步成为原子的,记住只有比较和赋值,没有获得预期值过程),用于实现多线程环境下的同步。CAS操作包括三个操作数:内存位置、期望值和新值。当执行CAS操作时,如果内存位置的值等于期望值,那么就将内存位置的值更新为新值,否则什么都不做。CAS操作是原子的,因此可以保证在多线程环境下的正确性。
CAS操作是由CPU底层的原子指令实现的,下图是我们根据CAS原理写出来的伪代码:
上述过程只是对CAS的拙劣的还原。实际上Java中的CAS操作是通过sun.misc.Unsafe类提供的方法实现的。Unsafe类底层是通过JNI调用C/C++代码来实现的。因此我们一般不直接使用。Java的开发者已经结合实际应用场景对CAS进行了封装并提供了安全易用的接口。比如我们接下来要讲的原子类。
多线程状态下修改公共资源情况的模拟:
并发对count执行俩次++操作:
我们发现虽然CAS规避掉了线程2对count造成的值的覆盖情况,可是线程2的++并没有执行,我们仍然没有得到正确的答案,怎么解决这种情况呢?解决方法就是加一个循环。CAS会返回操作是否成功,我们可以让CAS作为while循环判断的条件,循环里对CAS的参数进行更新,如果操作成功直接跳出循环,操作失败进行一遍循环,然后再执行CAS。
伪代码:
则我们上述例子就变成了:
这也是一些原子类的实现原理。
CAS的应用
原子类
标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于原子类来实现的
这些基于CAS实现的原子类提供了一种高效、线程安全的方式来处理共享数据,能够在多线程环境下确保数据的一致性和正确性。最重要的一点是,它们避免了锁的使用,相比使用锁的同步机制,基于CAS实现的原子类在高并发场景下具有更好的性能表现。由于CAS操作是在硬件层面上实现的,避免了线程切换和阻塞等开销。
例如:
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.incrementAndGet();//对0进行加一操作
这就是利用原子类AtomicInteger对0进行++操作。
伪代码:
自旋锁:
CAS有三个参数:内存位置,期望值,修改后后的值。并且会返回执行是否成功。
我们可以利用这些特点实现一个自旋锁:
CAS的ABA问题
即在我们获取到参数:期望值 到CAS比较并赋值的过程中,变量值被修改但又修改回去了。
在大部分情况,ABA问题并不会产生影响,但仍然有部分场景会产生影响。
比如:
取钱时我们卡了一下,气急败坏,遂狂点俩次,开启了俩个扣款线程,本来是不会出现问题的,其中一个线程会作废,但是如果这个时候朋友给我转来了一笔钱且恰好使我的账户余额与扣款之前一致,则这时那个本应作废的扣款线程正常执行。于是乎:你的钱被扣了俩次。
解决方案:引入版本号机制
版本号机制
出现上述情况的原因 ,是因为变量进行了增又进行了减,使得CAS察觉不到发生了变化。
如果我们引入一个标记:每次进行修改之后使得这个标记只能单向变化(加一),CAS除了判断数据是否被修改以外,还判断这个标记,这就完美解决了这个问题。这个标记就是:版本号
所以现在我们CAS操作变成:
1.读取当前数据的值和版本号。
2.将实际的数据和版本号,与期望的数据和版本号进行比较,如果匹配则更新数据,并让版本号+1,如果数据被更改或者版本号变高就不做任何操作。