文章目录声明:尊重他人劳动成果,转载请附带原文链接!学习交流,仅供参考!
- 一、什么CAS?
- 1、CAS简介
- 2、CAS的三个操作数
- 2、用处
- 二、应用场景及源码分析
- 三、等价代码实现
- 1、等价代码实现
- 四、CAS中的缺点(ABA问题),怎么解决?
- 1、什么是ABA问题?
- 2、自旋时间长
2、CAS的三个操作数CAS 是compareAndSwap的简称,用中文表达则为比较并更新,简单的说,预期原值A和从某一内存中取得的值V两者相比较,如果预期原值A和内存值V相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。
- 内存值(V)
- 预期原值(A)
- 新值(B)
解决多线程并发安全问题,以前我们对一些多线程操作的代码都是使用synchronize、Lock关键字,来保证线程安全的问题;但是锁机制会出现很多开销,例如加锁和释放锁会导致比较多的上下文切换和调度延时。并且像synchronize这种拥有不可中断的性质。则开销更大。
二、应用场景及源码分析但是CAS的解决方法则为:去内存中获取值(java是无法直接操作底层操作系统,是通过本地native方法来进行访问,这里的native方法是用C/C++写的,但是JVM还是提供了一个类,这个类(Unsafe)提供了硬件级别的原子操作。),获取到了内存值,然后和我们预期原值进行比较,如果相等则更新内存中的值。如果不相等则用do...while采取重试等措施。直到修改完成。
- 乐观锁
也叫非互斥同步锁、不会锁住被操作对象,底层是CAS实现
- 原子类
实现原子操作,底层也是CAS。用AtomicInteger举例
源码分析
1、获取Unsafe类
2、通过Unsafe类中的objectFieldOffset()获取value在内存的偏移量
3、 用volatile修饰value字段,保证可见性
4、查看AtomicInteger中的compareAndSet(),可以看到有两个参数expectedValue(预期原值),newValue(更新值)
然后再进入compareAndSetInt(),就可以看到是一个native方法。其中offset就是偏移量。
5、native方法在(C++)中
6、通过偏移量获取地址
7、通过Atomic::cmpxchg()实现原子性的比较和替换,其中参数x是即将更新的值,参数e是原内存中的值,参数addr是获取内存中值的地址
总结
AtomicInteger中是通过Unsafe类来操作底层,尤其是Unsafe类中的compareAndSwapInt方法,方法中先想办法拿到变量value在内存中的偏移量。然后C++代码中通过偏移量拿到在内存的具体地址,然后通过Atomic::cmpxchg实现原子性的比较和替换,其中参数x是即将更新的值,参数e是原内存中的值。至此,最终完成了CAS的全过程。
- 并发容器(ConcurrentHashMap)
CAS是通过cpu的特殊指令来保证了原子性。(必须需要cpu支持这个指令)
下面所有代码就可以等于cpu中的一条指令
public class CompareAndSwapDemo {
private volatile int value;
public synchronized int compareAndSwap(int exceptedValue, int newValue) {
int oldValue = value;
if (exceptedValue == oldValue) {
value = newValue;
}
return oldValue;
}
}
四、CAS中的缺点(ABA问题),怎么解决?
1、什么是ABA问题?
在执行conpareAndSwap的时候,它只是检查预期原值和内存中的值是不是相等,但是它并不检查在此期间是否被修改过,例如:线程一拿到这个值是5,然后就去计算了,在线程一计算的过程中,由第二个线程把5改为了7,然后又由第三个线程把7改会了5,等线程一计算完成后,去看当前值还是5,符合预期,那么它也会更新成功,从操作上看并没有什么不对,更新成功也是对的,但是这样是有隐患的。
解决方案
2、自旋时间长类似数据库乐观锁那种,添加版本号,
解释一下自旋:就是如果一个线程准备去更新值,但是每次获取的值都被其他线程修改了,那么它就会一直去进行比较,直到成功为止,如果一直更新不了的话,那么CPU开销就会很大,这种要避免,所以一般对于这种适合并发写入少。大多数是读取的场景
代码展示
利用了AtomicReference类实现了一个乐观锁,乐观锁底层是用CAS实现的。所以会出现用do...while自旋。此处我用的while
public class SpinLock {
private AtomicReference sign = new AtomicReference<>();
// 加锁
public void lock() {
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
System.out.println(Thread.currentThread().getName() + "获取锁失败,请重试");
}
}
// 解锁
public void unlock() {
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取乐观锁");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了乐观锁");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放到了乐观锁");
}
}
};
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
结果
中间省略了一部分太多了



