说在前面的话
正如我开篇所说,我要整理一些java并发编程的学习文档,这一篇就是第二篇:java中的CAS。
这一篇主要说的CAS的原理,以及java中的CAS的一些应用吧!
欢迎关注和点赞。
开整
CAS是个啥
CAS,compare and swap的缩写,中文翻译成比较并交换。
首先想说的就是:CAS只是一个思想,原理。 本身没有问题,也没有所谓的ABA问题,也没有所谓的乐观锁长时间占用资源的问题。这些问题都是因为我们使用CAS的方式导致的。 不信?看我细细道来…
CAS描述起来大概是: 准备一个预期值和新值,拿预期值和要修改的变量的空间中已有的值进行比较,如果一样,就使用新值替换原始值,否则就算了。 大致就是下面这个图: 图很丑,我再描述以下,使用预期值A和内存值X进行比较,如果相同,就使用新值B替换内存值X。 不相同就算了。 这就是所谓的比较并交换。
CAS可是NB的不行的原理和技术。java中的整个JUC包中的内容基本都是基于CAS的。
CAS为啥NB呢,因为CAS是基于CPU级别的指令执行的,那是真的快。
我来一段java程序模拟一个看看:
public class CASTest {
private static int x = 5;
public static void main(String[] args) {
boolean cas = cas(5, 50);
System.out.println(cas);
}
public synchronized static boolean cas(int v,int b){
if(v == x){
x = b;
return true;
}
return false;
}
}
这个程序看上去应该很好理解。 CAS中比较v和x的值,如果相同,就把b的值赋值给x,否则就什么都不做。
CAS和ABA问题
ABA问题在上一小节中有所提及,这里再次认真的分析一边。
实际上ABA问题并不是CAS的问题,CAS原理本身是没有问题的。ABA是因为一种操作造成的。
大致是这个意思:
(请理解一个头发被程序夺走的程序的美术功底。。。图很乱,看下面文字说明) 有个树洞(内存空间),里面有个数字3。
小黑先使用克隆机器从树洞中把数字3克隆一份出来,并且将克隆出来的3作为预期值,准备使用CAS将树洞里面的3更换为7。但是就是在这个时候,小黑女朋友找他,他就走神了。
此时小蓝来了,小蓝也是先把树洞里面的3克隆一份出来,将克隆的3作为预期值,并且使用CAS将树洞里面的3修改为5,但是立马发现有点问题,又立刻使用CAS把树洞里面的5替换回3。一套操作行如流水。
这时小黑那边女朋友的事情处理完了,才回来执行CAS准备将3替换为7。首先使用自己手中的3和树洞里面的3进行比较,发现一样(当然其实我们知道次3非彼3),于是就使用7替换了树洞里面的3。
这里的问题很明显,就是小黑取出内存的值之后,才进行比较就是这个时间差,树洞里面的值有可能已经被来回的替换了多次,最后正好又给换回了3,但是这个3已经不是原来的3了。 这就是ABA问题。
打字真累,看程序理解一下:
import java.util.concurrent.TimeUnit;
public class CASTest1 {
private static int x = 3;
public static void main(String[] args) {
new Thread(()->{
System.out.println("小黑开始");
int a = x;
System.out.println("小黑被睡了....");
try {
TimeUnit.MICROSECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(a == x){
x = 7;
}
System.out.println("小黑替换后的值:"+x);
}).start();
new Thread(()->{
System.out.println("小蓝开始了");
int a = x;
if(a == x){
x = 5;
}
System.out.println("小蓝第一次替换后的值:"+x);
a = x;
if(a == x){
x = 3;
}
System.out.println("小蓝第二次替换后的值:"+x);
}).start();
}
}
嗯…结果很明显了。
问题就是这么个问题,怎么解决呢?你要问怎么解决?
嗯…首先再说一遍,ABA是使用CAS的操作的问题,跟CAS本身无关,你只要不按照上面的方式操作就不会有ABA问题呀。
比如:预期值不是从变量x中获取的。也就不会有取值和比较的时间差的问题。自然就不会有ABA文件。当然在实际的使用中,我们很多时候都是上面的操作模式。
CAS都在哪里用了
CAS在JCU包中的很多类中都有用到。比如我们上一小节说的Atomic系列中。
哪到低是咋用的呢?
我们来翻翻源码,look look。
先来看看:AtomicInteger的compareAndSet方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
用了Unsafe类中的compareAndSwapInt方法。
再来看看 AbstractQueuedSynchronizer 类的 compareAndSetState()方法。
啥?AbstractQueuedSynchronizer是啥? AbstractQueuedSynchronizer。 你看这个单词跟AQS像不像?后面会有AQS的详解的文章。 欢迎关注。。。。
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
这里还是Unsafe类中的compareAndSwapInt方法。
好吧,来看看Unsafe类中的compareAndSwapInt方法:
额 。。。。。 此时此刻,我赋值这样的代码了:(看我的注释说明吧)
对了,还要提醒一句,Unsafe是直接操作内存的。我后面的文章也会有对Unsafe类的专门讲解,欢迎关注。
package sun.misc;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
public final class Unsafe {
public final native boolean compareAndSwapInt(Object obj, long offset, int expected, int x);
}
OK!此时我们也清楚了,JUC中所有用到的CAS都是Unsafe类中的CAS相关的方法。
至于都用到哪里了…嗯…
首先就是Atomic系列的类中的大部分方法都用到了,只要需要更新内存的值的方法都用到了。这个就不用多说了。
还有就是我们常说的乐观锁,比如synchronized的轻量级锁,还有就是ReentrantLock系列。还有部分同步集合类(这些类后面也会有讲的,欢迎关注呦。。。。)
关于在锁中的用法大致是这样的:
? 我们要知道所谓锁,就好比高铁上的厕所使用标识一样。在资源前面设置一个标识,当资源被占用(被锁)的时候这个标识是一个值,当资源空闲的时候,这个标识是另外一个值,任何一个线程要使用这个资源都要获取锁,所谓获取锁就是尝试将这个标识修改为被占用的状态,谁修改成功谁就可以使用这个资源。 所谓乐观锁就是使用循环不断的尝试使用CAS修改这个锁标识(大致就是你在上厕所的时候,外面有个人一直敲门,问你好了没…)。 OK这就是CAS在锁中的大致用法。
总结
? 最后总结以下,CAS本身其实很好理解,不需要长篇大论。但是还是写了这么多字,那是因为CAS在并发编程包中用的太多了。 CAS原理和技术,本身没有问题,CPU级别的指令,效率贼高。但是在使用过程中,由于我们的需求,可能会产生一些问题。这些问题,有的需要解决,有的不需要解决。无论如何我们要理解CAS,了解使用中的问题,只有这样才能必坑。
? 这就是,之前有同学问我,我毕业了就是写写CRUD,面试官为啥要问我这么多的高并发的问题。我的回答就是:你写程序的时候,就奔着一个目标,实现需求。 而我还会考虑另外一个问题,在高并发情况下,在分布式情况下我写的程序会不会有并发问题。 这就是此刻,你不需要解决高并发的问题,但是你的程序要准备好接受高并发的考验。
好了,这篇,就到这里了。如果觉得还不错,记得点赞。 欢迎关注…你的肯定,是我更新的动力。
|