推荐链接:
总结——》【Java】
总结——》【Mysql】
总结——》【Redis】
总结——》【Kafka】
总结——》【Spring】
总结——》【SpringBoot】
总结——》【MyBatis、MyBatis-Plus】
总结——》【Linux】
总结——》【MongoDB】
总结——》【Elasticsearch】
一、概念
CAS = Compare And Swap = 比较并交换
- 先比较一下值是否与预期值一致,如果一致,交换,返回true
- 先比较一下值是否与预期值一致,如果不一致,不交换,返回false
CAS是一种在
多线程
环境中进行操作的原子指令
。它的作用是对内存中的某个位置进行一次“乐观”的读取,并根据读取值与期望值是否相同来决定是否对该内存位置进行更新。
CAS 操作是一种无锁的写入方式
,允许多个线程并发的访问内存,并且通常比加锁操作的性能要高。
Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。
通过cmpxchg
指令实现,单核就采用cmpxchg,多核需要追加前缀lock指令保证只有一个线程在执行当前CAS。
二、参数
参数 | 描述 |
---|---|
内存位置(V) | 要进行操作的内存位置 |
期望值(E) | 将内存位置的值与期望值进行比较,如果相同则进行更新,否则不进行操作 |
更新值(U) | 当期望值与内存位置的值相同时,CAS 操作会将内存位置的值更新为更新值 |
三、结果
CAS 操作的结果是一个布尔值
,表示操作是否成功。
当期望值与内存位置的值相同时,CAS 操作会成功并返回 true,否则返回 false。
四、使用场景
- 实现各种无锁的同步机制,例如计数器、队列和栈等。
- 线程数较少、等待时间短可以采用自旋锁进行CAS尝试拿锁,较于synchronized高效
五、底层实现
最终回答:先从比较和交换的角度去聊清楚,在Java端聊到native方法,然后再聊到C++中的cmpxchg的指令,再聊到lock指令保证cmpxchg原子性
1、Java:Unsafe类中的native方法
CAS在Java层面就是Unsafe类中提供的一个native方法,成功返回true,失败返回false,如果需要重试策略,自己实现。
native是直接调用本地依赖库C++中的方法
/`
* 如果变量的值为预期值,则更新变量的值,该操作为原子操作,如果修改成功则返回true
*
* 4个参数:哪个对象,哪个属性的内存偏移量,oldValue预期值,newValue最终值
*/
public final native boolean compareAndSwapObject(Object o, long offset,
Object expected,
Object x);
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
public final native boolean compareAndSwapLong(Object o, long offset,
long expected,
long x);
2、C++:unsafe.cpp中的
Unsafe_CompareAndSwapInt执行cmpxchg指令
本地已经下载hospot源码:
\hotspot\src\share\vm\prims\unsafe.cpp
直接官网查看hospot源码:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/prims/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);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
3、linux:atomic_linux_x86.inline.hpp中的cmpxchg使用lock指令
本地已经下载hospot源码:
\hotspot\src\os_cpu\linux_x86\vm\atomic_linux_x86.inline.hpp
直接官网查看hospot源码:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp
cmpxchg是汇编指令,CPU硬件底层就支持cmpxchg。
cmpxchgl本身是原子性操作(不能再拆分的指令),但并不能保证原子性,所以需要判断当前系统是否为多核处理器,如果是多核,就添加lock前缀。
lock指令可以理解为CPU层面的锁,一般锁的粒度就是缓存行级别的锁,当然也有总线锁,但是成本太高,CPU会根据情况选择。
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();// 内联函数,用来判断当前系统是否为多处理器(如果当前系统是多处理器返回1,否则返回0)
// __asm__代表汇编,直接操作硬件,执行cmpxchgl指令
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" // 如果当前系统是多处理器(即mp值为1),则为cmpxchg指令添加lock前缀,否则不加lock前缀
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
六、基于CAS实现的类
- AtomicInteger
- LongAdder
- ReentrantLock
七、优点
什么是非阻塞式的呢?其实就是一个线程想要获得锁,对方会给一个回应表示这个锁能不能获得。在资源竞争不激烈的情况下性能高,相比synchronized重量锁会进行比较复杂的加锁,解锁和唤醒操作。
CAS是一种乐观锁,而且是一种非阻塞的轻量级的乐观锁
,不会挂起线程。
八、缺点
1、ABA问题
ABA不一定是问题!因为一些只存在 ++,–的这种操作,即便出现ABA问题,也不影响结果!
(1)现象
(2)解决方案
在修改value同时追加 版本号
(3)示例
JUC下提供的AtomicStampedReference(同时判断值和版本号)
public static void main(String[] args) {
AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA",1);
String oldValue = reference.getReference();
int oldVersion = reference.getStamp();
boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
System.out.println("修改1版本的:" + b);
boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
System.out.println("修改2版本的:" + c);
}
2、如果cas失败(自旋)次数过多占用CPU资源
(1)现象
while循环,cas一直没成功,会一直进行自旋,会额外的占用大量的CPU资源
Q:为什么会占用CPU资源?
A:因为CAS不会挂起线程,会让CPU一致调度当前线程执行CAS直到成功。
(2)解决方案
不同场景有不同的处理方案
方案 | 特点 | 具体实现 |
---|---|---|
synchronized的实现方式 | 自适应自旋锁 | 如果cas失败(自旋)次数过多(超过指定次数),就挂起线程(WAITING),避免占用CPU过多的资源 |
LongAdder的实现方式 | 分段锁 | 在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果 |
Q:LongAdder?
A:基于类似分段锁
的形式去解决(要看业务,有限制的)
传统的AtmoicLong是针对内存中唯一的一个值去++
现在的LongAdder
在内存中搞了好多个值,多个线程去加不同的值,如果自增失败,将失败的信息添加到Cell[],当需要结果时,将所有值累加再返回。
3、只能保证一个数据的安全
(1)现象
无法像synchronized一样锁住一段代码
(2)解决方案
ReentrantLock基于AQS实现,AQS基于CAS的方式实现了锁的效果。
九、CPU 实现原子指令
CPU 实现原子指令有 2 种方式:
- 1、通过总线锁定来保证原子性
- 2、通过缓存锁定来保证原子性
1、通过总线锁定来保证原子性
所谓总线锁定
就是使用处理器提供的一个 LOCK# 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。但是该方法成本太大。因此有了下面的方式。
2、通过缓存锁定来保证原子性
所谓 缓存锁定
是指内存区域如果被缓存在处理器的缓存行中,并且在 Lock 操作期间被锁定,那么当他执行锁操作写回到内存时,处理器不在总线上声言 LOCK# 信号,而是修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据(这里和 volatile 的可见性原理相同),当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
注意:有两种情况下处理器不会使用缓存锁定:
- 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
- 有些处理器不支持缓存锁定,对于 Intel 486 和 Pentium 处理器,就是锁定的内存区域在处理器的缓存行也会调用总线锁定。
十、CAS保证原子性示例
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}