前言
CAS(Compare and Swap)和Atomic原子操作是现代并发编程中的关键工具,它们为多线程环境下的数据共享和同步提供了强大的支持。本文将深入剖析CAS和Atomic操作的原理与应用,探讨它们如何在多线程程序中确保数据的一致性和线程安全性。无论您是初学者还是有经验的开发人员,都将从本文中获得有关并发编程的宝贵见解,使您能够更好地利用这些强大的工具来构建高效、可靠的并发应用程序。
CAS和Atomic原子操作
i++操作不是线程安全的
volatile只能保证可见性可有序性,无法保证i++的原子性
保证原子性的方法
- synchronized可以,但是需要切换到内核态,很消耗资源,i++不会去使用
- ReentrantLock lock() unlock()放到finally中
- CAS CAScompare and swap 比较并交换,修改旧的值,返回新的值
CAS操作修改变量V的值
- V:内存中的实际值,有可能被其他线程修改
- E:旧值,当前线程之前从内存中获取的值,也就是参与和V进行比较的值
- U:当前线程需要更新的值,也就是需要参与和V进行交换的值
- 读取变量V的值=5,赋值给E。(E=5) 此时可能会有其他线程进来修改V的值
- 计算E+1=6结果赋值给U
- 经过时间线,再次判断(E==V),如果相等的话说明其他线程没有修改,把U赋值给V,返回E
在操作系统层面保证这一段代码的原子性
if (V == E) { V = U }
Java实现CAS在Unsafe中,提供了
// 四个参数分别是:对象实例,字段的内存偏移量,字段的期望值,字段更新值 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);
单纯的CAS不能保证可见性,加上Lock前缀指令,就保证了原子性
偏移量
可以理解为变量在内存中的位置
例子:
class Entity(){ int x; }
对象头占8个字节,指针占4个字节(64位开启压缩指针),保证8的整数倍就会还有数据填充位。整个对象是16个字节,此时x的偏移量就是12。
如果改成
class Entity(){ int y; int x; }
这个时候x的偏移量就是从16开始
CAS在JVM中
- 根据偏移量计算value地址
- cas成功,返回期望值e, 等于e,方法返回true
- cas失败,返回内存中的value值,不等于e,方法返回false
#unsafe.cpp UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); // 根据偏移量,计算value的地址 jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); // Atomic::cmpxchg(x, addr, e) cas逻辑 x:要交换的值 e:要比较的值 //cas成功,返回期望值e,等于e,此方法返回true //cas失败,返回内存中的value值,不等于e,此方法返回false return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END
不管是 Hotspot 中的 Atomic::cmpxchg 方法,还是 Java 中的 compareAndSwapInt 方法,它们本质上都是对相应平台的 CAS 指令的一层简单封装。CAS 指令作为一种硬件原语,有着天然的原子性,这也正是 CAS 的价值所在。
CAS的缺陷:
- 一直自旋获取锁不成功,会导致cpu空转,给cpu打开很大的开销
- 只能保证一个共享变量的原子操作
- ABA
Atomic包,cas保证原子操作
ABA
问题描述:当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值A修改为B,又马上将其修改为A,此时其他线程不感知,还是会修改成功。
解决方法:使用版本号, AtomicStampedReference<V>
reference是实际存储的变量,stamp是版本,每次修改可以通过+1保证版本唯一性
public class AtomicStampedReference<V> { private static class Pair<T> { // 存储的变量 final T reference; // 版本号,每次修改+1 final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } private volatile Pair<V> pair; }
LongAdder/DoubleAdder
低并发、一般的业务场景下AtomicLong是足够了。如果并发量很多,存在大量写多读少的情况,那LongAdder可能更合适
LongAdder原理
AtomicLong中有个内部变量value保存着实际的long值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
最后
本期结束咱们下次再见👋~
🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗