目录
CAS原子性操作
Compare And Set(或Compare And Swap),CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)、新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值,否则一直循环(自旋)直到成功。
在java中可以通过锁和循环CAS的方式来实现原子操作。Java中 java.util.concurrent.atomic包相关类就是 CAS的实现。
CAS底层原理
这样归功于硬件指令集的发展,实际上,我们可以使用同步将这两个操作变成原子的,但是这么做就没有意义了。所以我们只能靠硬件来完成,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成。这类指令常用的有:
1. 测试并设置(Tetst-and-Set)
2. 获取并增加(Fetch-and-Increment)
3. 交换(Swap)
4. 比较并交换(Compare-and-Swap)
5. 加载链接/条件存储(Load-Linked/Store-Conditional)
CPU 实现原子指令有2种方式:
1. 通过总线锁定来保证原子性。
总线锁定其实就是处理器使用了总线锁,所谓总线锁就是使用处理器提供的一个 LOCK# 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。但是该方法成本太大。因此有了下面的方式。
2、通过缓存锁定来保证原子性。
所谓 缓存锁定 是指内存区域如果被缓存在处理器的缓存行中,并且在Lock 操作期间被锁定,那么当他执行锁操作写回到内存时,处理器不在总线上声言 LOCK# 信号,而时修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据(这里和 volatile 的可见性原理相同),当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
注意:有两种情况下处理器不会使用缓存锁定。
1. 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
2. 有些处理器不支持缓存锁定,对于 Intel 486 和 Pentium 处理器,就是锁定的内存区域在处理器的缓存行也会调用总线锁定。
CAS源码解析
JUC下的atomic类都是通过CAS来实现的,以AtomicInteger为例来阐述CAS的实现为例:
private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value;
public native long objectFieldOffset(Field var1);
Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问;不过尽管如此,JVM还是开了一个后门:Unsafe,它提供了硬件级别的原子操作。
证明
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger ai = new AtomicInteger();
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<Thread>();
// 添加100个线程
for (int j = 0; j < 100; j++) {
ts.add(new Thread(new Runnable() {
public void run() {
// 执行100次计算,预期结果应该是10000
for (int i = 0; i < 100; i++) {
cas.count();
cas.safeCount();
}
}
}));
}
//开始执行
for (Thread t : ts) {
t.start();
}
// 等待所有线程执行完成
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("非线程安全计数结果:"+cas.i);
System.out.println("线程安全计数结果:"+cas.ai.get());
}
/** 使用CAS实现线程安全计数器 */
private void safeCount() {
for (;;) {
int i = ai.get();
// 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值
boolean suc = ai.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
/** 非线程安全计数器 */
private void count() {
i++;
}
}
//结果:
非线程安全计数结果:9671
线程安全计数结果:10000
CAS采用自旋高效解决了原子性问题,存在三个缺点:
① 循环时间时间长的话会造成cpu大的开销:CAS自旋长时间不成功的话,会给CPU带来非常大的执行开销。
② ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 从Java1.5开始JDK的 atomic包里提供了一个类AtomicStampedReference 来解决ABA问题。这个类的 compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
public class CasABADemo {
private static AtomicStampedReference<Integer> asr = new AtomicStampedReference<Integer>(1, 1);
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(1000);
boolean flag = asr.compareAndSet(asr.getReference(), 2, asr.getStamp(), asr.getStamp() + 1);
System.out.println(" 1 ---> 2 -->" + flag + " 结果:" + asr.getReference());
flag = asr.compareAndSet(asr.getReference(), 1, asr.getStamp(), asr.getStamp() + 1);
System.out.println(" 2 ---> 1 -->" + flag + " 结果:" + asr.getReference());
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
int beforeStamp = asr.getStamp();
System.out.println("修改前版本号:" + beforeStamp);
Thread.sleep(2000);
int afterStamp = asr.getStamp();
System.out.println("修改后版本号:" + afterStamp);
//此刻已经放手ABA,1-->2-->1
boolean flag = asr.compareAndSet(asr.getReference(), 5, beforeStamp, asr.getStamp() + 1);
System.out.println("版本号匹配不成功:" + flag + " 当前版本:" + asr.getStamp() + " 传入的版本" + beforeStamp);
flag = asr.compareAndSet(asr.getReference(), 5, afterStamp, asr.getStamp() + 1);
System.out.println("版本号匹配成功:" + flag + " 当前版本:" + asr.getStamp() + " 传入的版本" + beforeStamp + " 最终理想值:" + asr.getReference());
}
});
t2.start();
}
//结果:
// 修改前版本号:1
// 1 ---> 2 -->true 结果:2
// 2 ---> 1 -->true 结果:1
//修改后版本号:3
//版本号匹配不成功:false 当前版本:3 传入的版本1
//版本号匹配成功:true 当前版本:4 传入的版本1 最终理想值:5
//结论:通过添加版本好-->解决了ABA问题.
}
③ 只能保证一个共享变量的原子性:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
public class CasDemo {
public static AtomicInteger ai = new AtomicInteger(0);
public static void add() throws InterruptedException {
ai.addAndGet(1);
Thread.sleep(1000);
ai.addAndGet(9);
System.out.println("计算和之后:" + ai.get());
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
es.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
add();
}
});
}
es.shutdown();
}
//分析:如果add()的输出结果都为10的整数倍,那么说明AtomicInteger保证方法原子性;反之不报账.
//结果:
// 计算和之后:32
//计算和之后:32
//计算和之后:32
//计算和之后:50
//计算和之后:41
//结论:CAS保证单个共享变量的原子性操作,但是不保证多个共享变量原子性和成员方法的原子性.
}