CAS
- 在高并发场景,可以使用加锁 或者CAS来保证原子性,但是加锁是很重量级的操作,CAS类似于乐观锁
- CAS ( Compare and swap )比较并交换,是实现并发算法时常用到的技术,包含三个操作数:内存位置、预期原值、更新值
- 执行CAS操作的时候,将内存位置中的值与预期原值比较
- 如果匹配,会将该位置的值更新为新值,
- 如果不匹配就不会做任何操作,或者重试,这种重试被称为自旋,多个线程同时执行CAS操作,只有一个会成功
- CAS 是JDK提供的非阻塞原子操作,通过硬件保证了比较-更新的原子性
- CAS 是一种系统原语,原语属于操作系统用于范畴,由若干条指令组成,用于完成某个功能,原语的执行必须是连续的,在执行过程中不允许被中断,所以说CAS是一条CPU的原子指令,不会造成数据不一致的问题
- JDK提供的CAS机制,在汇编层级会禁止变量两侧的指令优化,然后使用 cmpxchg(比较并交换) 指令比较并更新变量值
- 执行 cmpxchg 指令的时候,会判断当前系统是否为多核系统,
- 如果是就给总线加锁,只有一个线程可以对总线加锁成功,加锁成功后执行CAS操作
- 所以CAS的原子性实际上是CPU实现独占的,比起synchronized,CAS的排他时间要短很多,多线程情况下性能会更好
Unsafe类:
- Unsafe类是CAS的核心类,由于java无法直接访问底层,需要通过本地的 native 方法来访问,
- Unsafe想当与一个后门,基于该类可以直接操作特定内存的数据,内部的操作可以向C的指针一样直接操作内存,该类的方法基本都是native的,可以直接调用操作系统底层资源执行任务
- 但是实际工作中不要自己去使用 Unsafe类,容易导致内存混乱
- 如下,三个类似的方法,以第一个为例,参数分别为:
- var1 是操作的对象
- var2 是操作对象中属性地址的偏移量
- var4 期望的值
- var5 要修改的新值
- 核心思想就是比较内存中的值与预期原值进行比较,相等就更新为新值,例如:
- A、B两个线程都想要变更共享变量的值,各自都先读到主内存中的原值
- A执行较快,先完成计算,写回时判断主内存中的值和原值一样,就把计算得到的新值写回
- 这时B也执行完了,写回时判断主内存中的值和原值不一样,就放弃本次操作,这里可以加上自旋让B重新执行一次,这样在多线程情况下,两次计算结果就都不会丢失了
CAS自旋锁
- CAS利用CPU的指令保证了操作的原子性,达到锁的效果
- 自旋锁也就是获取锁失败的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,直到成功获取锁,或者超时,放在CAS就是执行一个CAS操作,不断的去执行CAS操作,直到CAS操作被成功执行
- 这样的好处是减少了线程上下文的切换,缺点是循环会消耗CPU
示例:不通过 synchronized 和 lock ,就实现了锁的功能,自己实现自旋锁
public class Caslock {
//是否加锁,初始值为 false,也就是未加锁
private AtomicBoolean atomicBoolean =new AtomicBoolean(false);
public void lock(){
System.out.println(Thread.currentThread().getName()+",尝试加锁");
//原子布尔的值是否是false,是就加锁,把值改为true,不是就释放锁
while (!atomicBoolean.compareAndSet(false,true)){
//不是false,加锁失败,由其他线程先加了锁,这里就需要等待
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()+",加锁成功");
}
public void unLock(){
//解锁,把值设为 false
atomicBoolean.compareAndSet(true,false);
System.out.println(Thread.currentThread().getName()+",释放锁");
}
}
private static void testCasLock() throws Exception{
Caslock caslock = new Caslock();
new Thread(()->{
caslock.lock();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
caslock.unLock();
},"线程A").start();
Thread.sleep(500);
new Thread(()->{
caslock.lock();
caslock.unLock();
},"线程B").start();
}
CAS的缺点
-
循环时间太长的话,开销会很大
-
ABA问题
- 例如A、B 两个线程,都拿到相同的初始值,A把值加1后写回,然后减1后又写回
- 此时B才执行完,尽管线程B的CAS操作成功,但是这样丢失了A的两次操作,所以仅仅只比较内容,是线程不安全的
想要解决ABA问题,需要加上版本号或者时间戳
- AtomicStampedReference:流水号的戳记引用