cas技术,或者说设计思想,其实挺让人着迷的,至少对于我来说是如此,这些天一直在研究这一块,今天写写针对cas的总结,会持续更新;
cas(compare and swap),中文翻译比较替换,大家又将它谈为乐观锁,说到锁,关于java里面太多了,而且还是面试官特别喜欢问的,简直让人头皮发麻,和乐观锁比较最多的就是悲观锁(synchronized)了。除此之外还有偏向锁,轻量级锁,重量级锁,自旋锁,类锁,数据库里面还有表锁,行锁,elasticsearch里面还有全局锁,文档锁,树锁,redis中的分布式锁等等;
我们平时应用多线程技术非常多,比如网上商店卖一类商品,多个用户同时去购买,列车卖票,多个用户同时购买同一车次的车票等等场景,这些都涉及到了一个问题,共享资源,在多个线程去访问共享资源的时候,如何去保证共享资源的一致性?在操作系统里面有一个临界区的概念,其实就是一个程序片段,这个片段在同一时刻,针对共享资源,只能一个线程去访问,这就是锁。那么这个程序片段。对应java里面是什么呢?是synchronized,被synchronized修饰过的程序片段,相当于加了锁,而这个锁,就是悲观锁。那么问题来了,如果一个系统中的共享资源是改多读少的情况,加锁为了保证数据的一致性无可厚非,但是如果这个系统中的共享资源是读多写少的情况呢?要知道,无论是读,还是写,进入该程序片段,都是要加锁的,而每加锁一次这涉及到了系统调用内核态和用户态之间的切换,频繁加锁,释放锁,这个代价是非常大的,所以我们提出了乐观锁的概念,乐观锁在应用层,其实是没有锁的。这也是CAS技术为什么多用于读多写少的应用场景。
CAS是怎么做到没有加锁就能保证数据的一致性的呢?
首先,CAS有三个概念,分别是V,A,B;V表示要修改的值,即共享资源,这个值可以是内存中,可以是数据库中,总之这个是你的共享资源,A表示更新的比较预期值,B表示即将要更新的新值。这么说可能有点不太理解,那么说说cas的操作流程就明白了。流程:V为共享资源,多个线程要去操作V,每个线程会先读取V的值,将值赋给A,其中一个线程要执行更新操作,这个时候,会将这个线程的A值和现在内存中的V值对比,如果没有变化,说明这个之前没有别的线程修改(这里不涉及ABA问题,ABA问题可以通过时间戳或者版本解决),于是将V的值更新为B,如果A值和V值不相等,则不更新或者重新获取A值。
这就是CAS的设计,不过,看到这里,大家有没有想过,如果有两个或者多个线程同时都经过了第一次的V和A值比较呢?那么到了更新一步是不是会出现数据不一致的情况?因为比较和更新其实并不是原子操作。
我们来讨论一下JAVA 8中是怎么解决这个问题的。
比较典型的原子操作对象,AtomicInteger,我们可以看一下里面的源码,自增1是怎么实现的
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
可以看到,首先get()从内存中获取内存值,在使用之前,你要自己声明一个volatile的所有线程可见变量。接着,加1赋值给了next,接着将current和next传给了compareAndSet函数,如果成功,则返回next值,这个时候,current和next就是分别对应上文的A,B。继续看compareAndSet是怎么保证原子操作的。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
可以看到,这里直接把值传给了unsafe的compareAndSwapInt方法,由它来保证原子操作,这里我就不贴源码分析了。我直接总结,这个unsafe其实是java中的jni(java native interface)技术调用另外语言的实现,这里是*.cpp,名字太长,我用*代替,这个操作呢其实是硬件层面的技术,涉及到底层的汇编,总的来说就是进入这个unsafe的函数后,在计算机内存运行中,同一时刻通过总线和缓存的锁定,从而实现了比较和替换,达到原子操作的目的,不得不说,看*.cpp文件是一件掉发的操作,珍爱发型,远离秃头;
说了半天,原来这里面最难的原子操作是在硬件层面呀,那我们有没有可能在应用层就能实现呢?答案自然是有的,这里我提供一个我自己的个人想法;
这里我采用的做法是调用已经写好的AtomicInteger来辅助。
1、声明一个针对该共享资源的volatile int变量value,初始化为0;
2、两个或者多个线程进入了第一步的V和A比较,获取value值,这个时候,多个线程就变成了串行化,而不用加锁,因为value是严格排序的;
3、获得value为1的线程执行更新操作,操作完之后,初始化为0,其余不为了1的线程重新获取V值或者处理丢弃;
那么还有一个问题,如果获取到1的线程在执行过程中死掉了怎么办?如果死掉了,那么岂不是这里一直就停住了,不能操作共享资源啦?因为只有value为1的线程才能进入临界区,所以,在这里,我们还要设置一个超时时间,如果超过设定时间,还是要杀掉现在操作资源的线程,重置value值为0和复原在此过程中改变的V值,以便让其他线程进入,当然这种情况几乎很少发生,但还是得考虑。
ABA问题就是线程1的A值去比较V值,发现相等,但是如果在这之前,线程2将V值改了,线程3又将V值改回来,那这样其实A值和V值是相等的,但其实里面已经有了变动,所以叫ABA问题,可以采用时间戳,在对比那一步,加入了时间戳对比就可以了,或者如果是elasticsearch中可以采用版本号比较。
cas应用层实现做法,大家也可以去看看disruptor框架的做法,里面提出了一个前门后门的概念,采用环形缓存做法,据说非常高效;
如果还有别的地方没有考虑到,欢迎大家提出,一起学习进步。
网友评论