深入理解CSA
锁机制存在以下问题
jdk5之前,我们知道,在多线程编程的时候,为了保证多个线程对一个对象同时进行访问时,我们需要加同步锁synchronized,保证对象的在使用时的正确性,但是加锁的机制会导致如下几个问题
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。
锁的分类:
- 悲观锁:独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
- 乐观锁:所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS
CAS,比较并交换(Compare-and-Swap,CAS),如果期望值和主内存值一样,
则交换要更新的值,也称乐观锁。
CAS的机制就相当于这种(非阻塞算法),CAS是由CPU硬件实现,所以执行相当快.CAS有三个操作参数:内存地址,期望值,要修改的新值,当期望值和内存当中的值进行比较不相等的时候,表示内存中的值已经被别线程改动过,这时候失败返回,当相等的时候,将内存中的值改为新的值,并返回成功。
为什么说原子类底层实现,是CAS;
https://www.cnblogs.com/chengxiao/p/6789109.html
package com.baidu.casDemo;
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
//CAS (compareAndSet) 比较并交换
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//对于我们平时写的SQL :乐观锁;
//期望,更新;
// public final boolean compareAndSet(int expect, int update) { 期望。 更新;
//如果我们的期望值达到了,那么就更新,没否则,就不给更新
//CAS 是CPU 并发原语;CPU的指令
//==============捣乱的线程============
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
//==============期望的值==============
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
缺点:
1:循环会耗时;
2:一次性只能保证一个共享的变量的原子性
3:ABA问题
atomicInteger.getAndIncrement();就是一个自旋锁
自旋锁;getAndAddInt就是一个自旋锁;
ABA
当然CAS也并不完美,它存在"ABA"问题,假若一个变量初次读取是A,在compare阶段依然是A,但其实可能在此过程中,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。
解决ABA问题:引用带版本号的原子引用:
注意:在下面的代码中Integer是一个大坑
Integer使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂方法valueof获取对象实例,而不是new,因为valueOf使用缓存,而new一定会创建新的对象分配新的内存空
间;
// 主内存共享变量,初始值为1,版本号为1
private static AtomicStampedReference<Integer> atomicStampedReference = new
AtomicStampedReference<>(1, 1);
public static void main(String[] args) {
// t1,期望将1改为10
new Thread(() -> {
// 第一次拿到的时间戳
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+" 第1次时间戳:"+stamp+" 值为:"+atomicStampedReference.getReference());
// 休眠5s,确保t2执行完ABA操作
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// t2将时间戳改为了3,cas失败
boolean b = atomicStampedReference.compareAndSet(1, 10, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName()+" CAS是否成功:"+b);
System.out.println(Thread.currentThread().getName()+" 当前最新时间戳:"+atomicStampedReference.getStamp()+" 最新值为:"+atomicStampedReference.getReference());
},"t1").start();
// t2进行ABA操作
new Thread(() -> {
// 第一次拿到的时间戳
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+" 第1次时间戳:"+stamp+" 值为:"+atomicStampedReference.getReference());
// 休眠,修改前确保t1也拿到同样的副本,初始值为1
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 将副本改为20,再写入,紧接着又改为1,写入,每次提升一个时间戳,中间t1没介入
atomicStampedReference.compareAndSet(1, 20, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName()+" 第2次时间戳:"+atomicStampedReference.getStamp()+" 值为:"+atomicStampedReference.getReference());
atomicStampedReference.compareAndSet(20, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName()+" 第3次时间戳:"+atomicStampedReference.getStamp()+" 值为:"+atomicStampedReference.getReference());
},"t2").start();
}
测试结果:
测试结果分析:
在多线程的情况下,我们的线程 t2 去进行了 ABA 的操作;将原来的主内存中的值,从1–20,又从20–1 的操作。这就是 ABA 操作。此时我们的 t1 的版本号已经被修改过了,所以CAS 为false;