高并发–CAS机制
在使用技术时也一定看看底层,这样在以后的学习中才能触类旁通,举一反三
什么是CAS
- CAS 的全拼是:Compare and Swap ,即 比较和替换
- 无锁编程
- 是一种常见的降低读写锁冲突,保证数据一致性的乐观锁机制
- 原子性问题(原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行)
CAS机制是用来解决什么问题的
开始呢,我们先来思考一个问题,在多个线程来访问一个资源对象的时候我们来如何保证线程安全呢?相信我们第一反应就是加互斥锁,问题这就来了,如果大部分的操作都是读的操作,加上互斥锁会在每次调用的时候都锁定线程,这不是傻了么,还有一种情况当同步代码块的耗时远小于线程切换时,加上锁不是更有些本末倒置么,会严重拖慢程序的性能,这也是上锁的弊端,这种上锁的方式也就是悲观锁,看到这个文章的小伙伴一定对锁有一定的概念了这里就不多说了,回到正题。
大家一定都知道synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高,针对这种情况JAVA也尝试对synchronized进行了优化,对锁之间的切换增加了过度,大家一定都知道,无锁,偏向锁,轻量级锁,重量级锁的概念,所之间转换时很耗时的,优化之后也没能明显的改善,
问题就来了:假如我们不想使用互斥锁,还想对线程调用进行协调?
和你一样牛的前辈也想到了这个问题于是CAS诞生了
CAS是怎么工作的
还是上面的大前提——多个线程来访问一个资源对象
给大家举个例子:
假设我们的资源对象是一辆摇摇车,有A,B两个小孩想要玩儿,摇摇车有两个状态有空,没空,分别用0,1代表A,B两个小孩看待摇摇车上没人,就抢着往摇摇车跑去,小孩A先跑到并且开启摇摇车,此时摇摇车状态是1,小孩B跑到旁边时发现已经有人在玩了,虽然感觉挺不服气,但是秉着先到先得原则,只能在旁边等着, 现在回头想一想,当资源对象的状态为0的一瞬间,A,B两个线程同时读到状态值0,此时两个线程会各自产生一个值(资源对象的状态值——old Value),当线程得到资源对象更新状态后新的状态值(new Value),old Value一开始都是0new Value都是1,两个线程争抢着屈改变资源对象的状态值,假设线程A运气比较好,率先获得时间片,他讲自己的old Value与资源对象的状态值进行比较(Compare )发现一致,于是将资源对象的状态值改(Swap )为new Value,线程B慢了一步,这是资源对象的状态值已经被线程A改为1,所以线程B在比较(Compare )时会发现和自己预期的old value不一样,所以放弃了Swap操作 ,在实际应用中,我们不会让线程B直接放弃,我们会让线程B进行自旋(不断进行C/S操作),小孩B第一次没有坐上摇摇测也不可能轻易放弃,会在旁边等待(自旋),某一瞬间摇摇车状态为0时再次参与争抢,当小孩B没耐心也会离开不再争抢(设置初始值,达到初始值时停止尝试)
int cas( long addr, long oldvalue, long newValue)
{
if(addr != oldValue)
return 0;
addr = newValue;
return 1;
}
上面的代码很容易看出问题,没有任何同步操作线程一定是不安全的
解决办法:CAS必须是原子性的,比较数值是否一致和更新状态值两个动作一定要有一条线程进行操作,加锁?
似乎问题又回去了,又到了加锁的问题
不同架构的CPU给我们提供了指令级别的指令操作
-
X86架构下通过cmpxchg指令支持CAS
-
arm架构下通过LL/SC
实现不依赖锁来进行线程同步,但并不意味无锁代替无锁,这些通过CAS来实现的同步的工具由于不会锁定资源,而且当线程需要修改共享资源的对象时总是乐观的认为对象的状态值没有被修改过,而只是每次都会主动的尝试去比较状态值,这种机制也被狭义的理解为乐观锁,由于没有用到过锁是一种无锁的同步机制*
案例代码实现
看文章的小伙伴们在学习线程的时候一定写过这个代码
static int num