多线程【CAS】
一、CAS 是什么
CAS:compare and swap的缩写,中文翻译成比较并交换。
二、CAS的原理
CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。
V 表示要更新的变量(当前内存中的值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。
CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
三、CAS 的底层原理
CAS 是通过调用 JNI(Java Native Interface) 的代码实现的。
例如,在 JUC 包中的 AtomicInteger 类就使用到了 CAS,然后其就是通过 Unsafe 类调用本地方法来实现的 CAS 的。
Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe 类存在 sun.misc 包中,其内部方法操作可以像 C 的指针一样直接操作内存。
注意 Unsafe 类的所有方法都是 native 修饰的,也就是说 Unsafe 类中的方法都直接调用操作系统底层资源执行相应的任务。
为什么Atomic修饰的包装类,能够保证原子性,依靠的就是底层的 Unsafe 类。
四、CAS 的使用
JUC 包下的原子包装类和原子引用类都使用到了 CAS,可以去查看下这些原子类的源码。
五、CAS 的缺点
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题:
ABA问题,循环时间长造成开销大、只能保证一个共享变量的原子操作
(一)ABA 问题
CAS 算法实现一个重要前提是需要取出内存中某时刻的数据,然后和当前时刻的值进行比较。但是在这个时间差中的数据是如何变化的却无法察觉。
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。
(二)循环时间长开销大
如果某个线程更新失败的时候,会通过自旋来不断的进行尝试。但是自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
(三)只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。
从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
六、针对 CAS 中 ABA 问题的解决
部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。
如:JUC 包下的 AtomicStampedReference 类就是引用了这一思想来解决了ABA 问题,核心代码如下:
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))); }
我们可以发现,其不仅比较了两个时刻的值,还比较了两个时刻的版本号,通过这样的方式就能对数据监视整个时间差,而不仅仅是两个时间点,从而巧妙的解决了 ABA 问题。
Java新手,若有错误,欢迎指正!