java CAS学习
CAS Compare And Set的缩写, 在硬件cpu的支持下,它能够保证数据读取、写入的原子操作。该方法能够保证数据在多线程并发的环境下,数据写入的原子性。避免了多线程操作所造成数据写入的混乱。
在java语言中,所有java.util.concurrent.atomic.Atomic*的类都实现了CAS的方法,以AtomicInteger类为例:
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
方法需要传递两个值,第一个参数为期望值,第二个参数为预期更新后的值;如果内存中存在的值跟期望值相等,则更新为update并返回true;否则反之。
该方法通过调用Native本地方法实现,为多线程计算数值(包含读取-> 计算 -> 写入)的数据一致性提供了天然的保护屏障。
个人认为它跟volatile关键字即相互合作又相互区别
他们之间的区别在于:volatile 关键字提供了一种多线程间数据的可见性,我把它理解成透明度,即通过volatile关键字修饰的属性对每个线程透明,各个线程能够读取最新的volatile修饰的属性;但是很关键的一点:volatile不保证数据的原子性操作。而这一点正是CAS算法能够提供的。
根据二者的特性,将他们进行组合使用,理论上既能保证数据的原子性也能保证数据的可见性.目前笔者没有对此进行过深入的研究,如果有同学有这方面的心得,还望不吝赐教。
扯的有点远了,下面的demo能够作为一个反例,测试出CAS方法调用情况:
/**
* 说明:
* 10个线程 每个线程内部循环100次 进行累加
* 如果最终结果是1000 则说明调用CAS方法没有达到预期的效果
*/
public class CasTest {
public static void main(String[] args) throws Exception {
int outTimes = 10,innerTimes = 100;
CountDownLatch latch = new CountDownLatch(outTimes * innerTimes);
AtomicInteger sum = new AtomicInteger(0);
for(int i = 0;i < outTimes;i++){
new Thread(()->{
for(int j = 0; j < innerTimes;j ++ ) {
latch.countDown();
int current = sum.get();
int next = current + 1;
boolean flag = sum.compareAndSet(current, next);
if(!flag){
System.out.println(String.format("current = %d,next = %d",current,next));
}
}
}).start();
}
System.out.println("正待执行结果!");
latch.await();
System.out.println(sum);
}
}
CAS ABA的问题描述:
多线程并发的条件下,CAS 算法所计算的值可能已经经过了N次的计算后,最后返回一个初始值;而其中的一个线程A由于并发的关系在最后终于获取锁的情况下,再用初始值B跟内存值(经过计算后的初始值B)进行CAS操作; 此时需要注意的是线程A的初始值跟内存值相同,但是严格意义上来说, 二者不是同一个东西,即此值非彼值。
下面的代码显示了我ABA问题的理解:
import java.util.concurrent.atomic.AtomicInteger;
/**
* ABA 问题测试代码
* 系统设置初始值为1,并启动三个线程
* 1. 线程A取出数据, 此时A线程暂停2秒
* 2. 线程B取出数据, 此时B线程暂停1秒
* 3. 线程C取出数据, 此时C线程取出数据并设置成初始值
*/
public class CasABATest {
public static void main(String[] args) {
AtomicInteger data = new AtomicInteger(1);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try{
Integer current = data.get();
Thread.sleep(2);
data.compareAndSet(1,100);
}catch (Exception e){
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try{
Integer current = data.get();
data.compareAndSet(1,2);
Thread.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try{
Integer current = data.get();
data.compareAndSet(2,1);
}catch (Exception e){
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
}catch (Exception e){
e.printStackTrace();
}
System.out.println(String.format("last data = %d",data.get()));
}
}
针对CAS ABA的问题现象,可以利用数据的版本号防止,JDK也已经提供了AtomicStampedReference类的方法来避免此来问题:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp);
CAS与自旋锁的关系:
首先需要明确一点:CAS是乐观锁的一种实现算法,同时也是构成自旋锁实现的基础。
自旋锁的概念:在多线程的环境下,由于锁竞争的关系,锁资源被其它线程占有,其他线程一直在循环等待(死循环),一直等到获取锁资源为止,才结束循环并释放锁。
从字面理解来看,自旋锁通过死循环一直在不断的索取锁资源;
优势:自旋锁的算法减少了线程上下文之间的频繁切换,在一定程度上可以提升性能
劣势:如果参与竞争的线程过多,则会造成大部分线程一直处于循环等待当中,会导致CPU资源的飙升。
JDK源代码很多地方都用到的CAS,如:
AtomicBoolean中的getAndSet方法,通过do while不断的循环、直到比较设置成功为止,才结束循环。
public final boolean getAndSet(boolean newValue) {
boolean prev;
do {
prev = get();
} while (!compareAndSet(prev, newValue));
return prev;
}