起初,笔者本人第一次看到这个简写,cas,也是不了解的;但是对于笔者的性格,究其所以然。那么整起:
我们先看下这个词的表述:
CAS:Compare and Swap,比较再交换
注意:此处我们咬文嚼字,比较再交换,比较再交换,比较再交换
重要的事情读三遍,中国文化博大精深,相信各位很好理解:也就是,需要先比较之后,再进行交换。
那么具体出现在什么地方呢?上源码:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
这段代码在我们的jdk中的
sun.misc.Unsafe下
细细分析,第一眼,看到此方法,会发现native这个关键字,那么这个是什么意思呢?
native是一个计算机函数,一个Native Method就是一个Java调用非Java代码的接口。方法的实现由非Java语言实现,比如C或C++。
所以,可以知道,该方法是直接由汇编底层实现的方法,直接发送指令到机器,所以,我们可以看到,这里没有实现内容。
所以我们大可认为,这个方法,是不会存在多线程问题的。
那么上述,我们描述“比较再交换”,就是此处,那么是怎么个“比较再交换”呢?
可以看到
此处有四个入参:都分别是什么意思呢?(该类没有注释)
这么解释
我们需要把 Integer age = 14; 改为 age = 15 ;那么age的对应的位置是 2134213;
那么就是 compareAndSwapInt(age, 2134213, 14, 15);
这样是不是更好理解一些?
用稍微正确的语言描述就是 var1为 所被修改的对象, var2 为对应的属性偏移量, var4就是对应的修改前的值, var5就是需要被交换后的值。
也就是:执行该方法,会去对应var2上对应的地址指向的值比较 是不是 和var4的值一致,如果一致,那么将会将var5的值进行赋值,执行交换动作。
这个时候衍生出一个新的东西,就是大家有没有发现,这个方法是有返回值的,就是boolean类型,返回的true和false是什么情况才会返回的呢?
当比较对应属性偏移量var2对应的值和var4所传入的值一致,那么就认为达到预期,那么将执行与var5进行交换。那么就是,如果预期比较一致,那么将返回true,并且交换成功了。如果与预期比较不一致,那么将返回false,那么就交换不成功。这也是返回值的一个条件。
那么这个返回值有何作用呢?上源码!
可以看到
java.util.concurrent.atomic.AtomicInteger
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这个方法就引用了Unsafe中的compareAndSwapInt();方法。
再看下引用的地方(同一个类AtomicInteger )
/**
* Atomically updates the current value with the results of
* applying the given function, returning the previous value. The
* function should be side-effect-free, since it may be re-applied
* when attempted updates fail due to contention among threads.
*
* @param updateFunction a side-effect-free function
* @return the previous value
* @since 1.8
*/
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}
*使用的结果以原子方式更新当前值
*应用给定的函数,返回上一个值。 的
*该功能应无副作用,因为它可能会重新应用
*当尝试更新由于线程间争用而失败时。
这个描述比较偏国外哈,那么用我的语言描述就是:会使用原子性的形式对一个对象进行更新值,该方法没有坏处,在多线程的环境下对同一个对象进行读写操作,如果执行“比较再交换”失败,那么将继续执行相同的操作。
那么,我们分析一下代码结构,执行流程哈。
可以看到,定义了两个整型值 prev, next;
执行do while语句进行循环。
首先执行一次结果代码块:获取到prev最新的值,再计算出需要替换后的值next,
然后调用compareAndSet()方法,判断这个刚刚获取到的值prev是不是和现在属性偏移量中的值一致,能不能更新?
如果不一致,不能更新,那好,代码再次去获取到prev最新的值,再计算出需要替换后的值next;
然后调用compareAndSet()方法,判断这个刚刚获取到的值prev是不是和现在属性偏移量中的值一致,能不能更新?
如果不一致,不能更新,那好,代码再次去获取到prev最新的值,再计算出需要替换后的值next;
然后调用compareAndSet()方法,判断这个刚刚获取到的值prev是不是和现在属性偏移量中的值一致,能不能更新?
........
直到刚刚获取的值prev和指针指向的值一致,更新成功,返回true,
然后 !true 不满足条件,退出循环。
返回 当前系统中最新的值。
这个动作就可以称之为“自旋锁”
那么cas就是可以表达清晰了,通过比较再替换来表达其核心。通过“自旋锁”的实现方式来表达其内部的实现逻辑。
那么很多地方会对cas比较synchronized两者的区别:
此处我简单分析一下:
1.通过比较两者的保持代码的原子性的实现方式,我们可以知道,sync是通过控制整个代码块来实现原子性的。而cas则是通过判断比较而交换来实现原子性的。通过两者的特性,可以知道,sync是一种类似某个时间点只能有一个线程访问该代码块,其他线程均同步阻塞。那么cas则是多线程间竞争访问该变量。如果在更新前,该变量被其他线程抢占,并更新,那么当前线程将进入锁,执行自旋锁,直至成功访问。这也是两者最大的区别,本质的区别,
通过此处,我们也可以究其效率性能问题探究一下:
cas并不会产生阻塞,而是不断线程间竞争。sync是严格杜绝其他线程访问同一数据。
两者可以猜出,cas在对于少量线程的时候,效率是大于sync;
sync在大量线程的时候,效率是大于cas的,并且在大量线程的时候,cas是需要消耗大量cpu的。
有兴趣的同学可以尝试比较一下两者具体性能差。
那么两者优缺点也油然而生。
但是cas还有一个很大的问题,笔者在初次学习的时候,也发问了。就是了解了cas的实现逻辑,想,这样不就有问题了吗?但是又被自己给否认了。觉得多虑了。
简单描述一下问题:假设线程A和线程B同时修改同一个对象。线程A执行效率为5s,线程B执行效率2s,该对象初始值为a,线程B将其改为c,然后又改为a,这个时候线程A计算完成了,判断比较的时候,发现获取主存中该对象的初始值和当前获取的值为一致,都是a,那么就将a改为d了。
那么这个时候,虽然线程A获取的值无变化,但是实际意义上,B都对其进行两次操作了。也就破坏了其原子性的概念。那么这就是 “ABA”问题了。
那么怎么解决该问题呢?
如何杜绝线程B在线程A不知情的情况下修改了两次数据,并且线程A还认为没修改呢?
那么解决方案,就是提供每次修改的日志记录,如果我们发现这次修改的记录的版本和获取的记录的版本不一致,那么就是已经被修改了。
那么JUC(java.util.concurrent)中也提供了一个解决方案:AtomicStampedReference
/**
* Creates a new {@code AtomicStampedReference} with the given
* initial values.
*
* @param initialRef the initial reference
* @param initialStamp the initial stamp
*/
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
如果我们不能通过结果判断出这个对象,值是否被修改过,这个“Object”还是不是之前的“Object”了,那么我们就可以引用上述类:原子标记
通俗来来讲:就是我们记录每次原子性替换的动作。怎么记录呢?是的,我们可以通过版本号,每一次修改都是原子性的,版本号并不会被覆盖修改。那么就是说。我们每次都可以记录这个对象,值被修改后的版本号。那么我们就可以知道:这个版本1的“Object”已经变成了版本4的“Object”了。这样,我们就知道了当前数据已经被修改,那么我们就需要重新获取最新的值了。
有想法的可以看看,该类,该类没有无参构造,也就是说我们必须入参初始化。
initialRef:初始化值
initialStamp:初始化版本号
AtomicStampedReference ref = new AtomicStampedReference<Integer>(100, 1);
那么我们使用的时候就需要对其赋予值和版本号。
此处我声明一个对象,初始值为100 版本号为1。
/**
* Atomically sets the value of both the reference and stamp
* to the given update values if the
* current reference is {@code ==} to the expected reference
* and the current stamp is equal to the expected stamp.
*
* @param expectedReference the expected value of the reference
* @param newReference the new value for the reference
* @param expectedStamp the expected value of the stamp
* @param newStamp the new value for the stamp
* @return {@code true} if successful
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
可以看到此处的匹配再替换,只有一个方法,并且有四个参数,按照顺序来:
expectedReference:预期值
newReference:新值
expectedStamp:预期版本
newStamp:新版本
那么这个时候,我们就可以考虑,如果当前版本乐观认为没有被修改,那么将,预期值和预期版本都是我们所知道的,也就是100和1
这个时候,我们就需要把版本升级和把新值赋予就可以了。
那么返回值也是boolean。来判断我们的结果是否替换成功。
具体问题我们就讲到这里。
下次我们来聊聊ArrayList为什么不安全?