博主个人社区:开发与算法学习社区
博主个人主页:Killing Vibe的博客
欢迎大家加入,一起交流学习~~
上篇总结了以下多线程场景下常见锁的策略,这篇总结一下CAS机制引起的ABA问题,以及解决方式。
乐观锁的一种实现,程序并不会阻塞,只会不断重试。
CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
标准库提供了 java.util.concurrent
包(juc包,并发工具包,包含原子类,线程安全集合比如ConcurrentHashMap,CopyOnWriteArrayList等等)
举个栗子:
比如定义一个整型变量int i = 0:
i++ 或者 i – 这些都是非原子性的操作,多线程并发会有线程安全问题。这个时候就要使用原子类保证它的线程安全性。
那什么是原子性呢?
就是指该操作对应CPU的一条指令,这个操作不会被中断,要么全部执行,要么全不执行,不会存在中间状态,那么这种操作就是一个原子性操作。
比如int a = 10
这种就是直接将常量10赋值给a变量,是一个原子性操作,要么赋值成功要么失败。
再比如a += 10
这种就是非原子性的操作,先要读取当前a变量的值,然后到寄存器中进行a+10计算,最后将计算得出的值重新赋值给a变量(对应了三个原子性操作)
所以我们可以使用原子类来保证线程安全性,比如常见的AtomicInteger
类:
//有参构造可以传入参数,传入多少就从多少开始计数,无参构造默认为0.
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();
看下源码:
通过形如上述代码就可以实现一个原子类. 不需要使用重量级锁, 就可以高效的完成多线程的自增操作.
使用CAS来实现自旋锁,乐观锁的一种实现
自旋锁指的是在获取锁失败的线程不进入阻塞态,而是在CPU上空转(线程不让出CPU,而是跑一些无用指令),不断查询当前锁的状态。
伪代码:
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
解释一下CAS(this.owner, null, Thread.currentThread())
操作:
只有当this.owner == null , 即当前自旋锁没有被任何线程持有 =>
就尝试将this.owner ==Thread.currentThread() ,将持有锁的线程设置为当前线程
有两个线程t1和t2,同时修改共享变量num,初始num == A
正常情况下,只有一个线程会将num 改为正确值,另一个线程在修改时num != A,另一个线程的工作内存的值已经过期了,因此无法修改
但如果线程 t2 经过多次修改后又回到了同一个值,此时线程 t1 会以为线程 t2 没被修改过,然后就去修改值 . 此时t2之间做的各种修改 其实对 t1来说都是不可见的 。
下面是ABA问题的图解:
为了解决ABA问题,就要引入版本号机制。
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
详情可以看下面这个链接:
以上就是多线程CAS机制的详解了,后续博主会更新Synchronized 原理 和JUC的常见类及用法,欢迎关注,点赞+收藏~~~