CAS和Atomic类
概述
CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。
案例
package com.test.cas;
import java.util.concurrent.CountDownLatch;
public class VolatileTest4 {
private static volatile int race = 0;
private static void incr() {
race++;
}
public static void main(String[] args) throws InterruptedException {
//CountDownLatch latch = new CountDownLatch(20);
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
incr();
}
//latch.countDown();
}
}).start();
}
// 等全部都运行完毕再继续执行主线程
//latch.await();
while(Thread.activeCount() >1) {
Thread.yield();
}
System.out.println("race:" + race);
}
}
可以确定,无论race字段加不加volitile,都会出问题,预期是200000,实际不是。这是因为race++
是一个几个步骤的操作,无法保证这几个步骤的原子性的问题。
那如何改?
只要加synchronized即可,
private synchronized static void incr() {
race++;
}
或者改用java.util.concurrent.atomic.AtomicInteger
package com.test.cas;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileTest3 {
private static AtomicInteger race = new AtomicInteger(0);
private static void incr() {
race.addAndGet(1);
}
public static void main(String[] args) throws InterruptedException {
//CountDownLatch latch = new CountDownLatch(20);
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
incr();
}
//latch.countDown();
}
}).start();
}
// 等全部都运行完毕再继续执行主线程
//latch.await();
while(Thread.activeCount() >1) {
Thread.yield();
}
System.out.println("race:" + race);
}
}
为什么AtomicInteger可以?
跟踪其代码
private static final Unsafe unsafe = Unsafe.getUnsafe();
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
可以看到接口的逻辑:
- 首先获取最新的v值(这个值就是预期值)
- 再传给
compareAndSwapInt
参数:预期值v,新值v+delta
等 - 如果所要修改的内存地址,其值等于预期v,说明中途未被其他线程修改,则用新值替换该内存地址上的值(即旧值),修改成功将预期值(即旧值)返回,至此结束。
- 如果其值不等于预期v,说明中途被修改了,则重复1、2、3直到成功为止
(这里补充下接口Object和offset字段的含义,应该指要修改o这个对象的offset字段,至于为什么叫offset,我估计是对象占用一定内存,用offset来确定这个字段在内存的哪个偏移量上)
CAS详解
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
CAS的缺点
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
- 循环时间长开销很大。
- 只能保证一个共享变量的原子操作。
- ABA问题。
注意: 不断循环直至成功是sun.misc.Unsafe的API,不是CAS底层指令本身,底层的就操作一次,如果一次新值和预期不一样就不再尝试。是Unsafe的Java类自己实现的循环尝试,并且compareAndSwapXXX只尝试一次,只有getAndAddXXX和getAndSetXXX才实现了循环尝试
循环实际长开销大
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
ABA 问题
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
参考资料
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/135327.html