声明:尊重他人劳动成果,转载请附带原文链接!学习交流,仅供参考!
文章目录
一、什么CAS?
1、CAS简介
CAS 是compareAndSwap的简称,用中文表达则为比较并更新,简单的说,预期原值A和从某一内存中取得的值V两者相比较,如果预期原值A和内存值V相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。
2、CAS的三个操作数
- 内存值(V)
- 预期原值(A)
- 新值(B)
2、用处
解决多线程并发安全问题,以前我们对一些多线程操作的代码都是使用
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)
三、等价代码实现
1、等价代码实现
CAS是通过cpu的特殊指令来保证了原子性。(必须需要cpu支持这个指令)
下面所有代码就可以等于cpu中的一条指令
/**
* @author delingw
* @version 1.0
* CAS等价代码
* 先比较再更新
*/
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
/**
* @author delingw
* @version 1.0
* 写一个乐观锁
*/
public class SpinLock {
private AtomicReference<Thread> 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();
}
}
结果
中间省略了一部分
太多了