简单的实现CAS
需求有100个线程同时访问,并且每个线程发起10次请求,最后count次数应该是1000次。
public class Demo {
//总访问量。volatile保证多线程之间count变量的可见性
private volatile static int count = 0;
/**
* count ++ 操作实际上是由3步来完成!(jvm执行引擎)
* 1.获取count的值,记做A : A=count
* 2.将A值+1,得到B :B=A+1
* 3.将B值赋值给count
* 修改升级第3步的实现:(compareAndSwap方法)
* 1.获取锁
* 2.获取count最新的值,记做LV
* 3.判断LV是否等于A,如果相等,则将B的值赋值给count,并返回true,否则返回false
* 4.释放锁
*/
//模拟访问的方法
public static void request() throws InterruptedException {
//模拟耗时5ms
TimeUnit.MILLISECONDS.sleep(5);
//表示期望值
int expectCount;
while (!compareAndSwap((expectCount = getCount()), expectCount + 1)) {}
}
/**
* @param expectCount 期望值count
* @param newCount 需要给count赋值的新值
* @return count当前值和期望值expectCount一致返回true
*/
public static synchronized boolean compareAndSwap(int expectCount, int newCount) {
//判断count当前值是否和期望值expectCount一致,如果一致,将newCount赋值给count
if (getCount() == expectCount) {
count = newCount;
return true;
}
return false;
}
public static int getCount() {return count;}
public static void main(String[] args) throws InterruptedException {
//开始时间
long startTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
}).start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "耗时:" + (endTime - startTime) + ",count = " + count);
}
}
JDK CAS支持
- CAS 全称“CompareAndSwap”,中文翻译过来为“比较并替换”
定义:- CAS操作包含三个操作数————内存位置(V)、期望值(A)和新值(B)。
- 如果内存位置的值与期望值匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不作任何操作。
- 无论哪种情况,它都会在CAS指令之前返回该位置的值。(CAS在一些特殊情况下仅返回CAS是否成功,而不提取当前值)
- CAS有效的说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置的值,只告诉我这个位置现在的值即可。”
怎么使用JDK提供的CAS支持?
- java中提供了对CAS操作的支持,具体在sun.misc.unsafe类中,声明如下:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
参数var1:表示要操作的对象
参数var2:表示要操作对象中属性地址的偏移量
参数var4:表示需要修改数据的期望的值
参数var5:表示需要修改为的新值
CAS实现原理是什么?
- CAS通过调用JNI的代码实现,JNI:java Native Interface,允许java调用其它语言。
而compareAndSwapxxx系列的方法就是借助“C语言”来调用cpu底层指令实现的。
以常用的Intel x86平台来说,最终映射到的cpu的指令为“cmpxchg”,这是一个原子指令,cpu执行此命令时,实现比较并替换的操作!
CAS源码
两个关键点:
- 自旋;
- unsafe类。
当点开compareAndSet方法后:
// AtomicInteger类内部
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
- 通过这个方法,我们可以找出AtomicInteger内部维护了volatile int value和private static final Unsafe unsafe两个比较重要的参数。(注意value是用volatile修饰)
- 还有变量private static final long valueOffset,表示该变量在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
- 变量value用volatile修饰,保证了多线程之间的内存可见性。
// AtomicInteger类内部
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;
- 然后我们通过compareAndSwapInt找到了unsafe类核心方法:
//unsafe内部类
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
-
AtomicInteger.compareAndSwapInt()调用了Unsafe.compareAndSwapInt()方法。Unsafe类的大部分方法都是native的,用来像C语言一样从底层操作内存。
-
这个方法的var1和var2,就是根据对象和偏移量得到在主内存的快照值var5。然后compareAndSwapInt方法通过var1和var2得到当前主内存的实际值。如果这个实际值跟快照值相等,那么就更新主内存的值为var5+var4。如果不等,那么就一直循环,一直获取快照,一直对比,直到实际值和快照值相等为止。
-
比如有A、B两个线程
一开始都从主内存中拷贝了原值为3;
1、A线程执行到var5=this.getIntVolatile,即var5=3。此时A线程挂起;2、B修改原值为4,B线程执行完毕,由于加了volatile,所以这个修改是立即可见的;
3、A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存的值不等于快照值3,所以继续循环,重新从主内存获取。
4、线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直至成功。
现代计算机动不动就上百核心,cmpxchg怎么保证多核心下的线程安全?
- 系统底层进行CAS操作的时候,会判断当前系统是否为多核心系统,如果是就给“总线”加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作,也就是说CAS的原子性是平台级别的!
什么是ABA问题?
- CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,在CAS方法执行之前,被其它线程修改为了B、然后又修改回了A,那么CAS方法执行检查的时候会发现它的值没有发生变化,但是实际却变化了。这就是CAS的ABA问题。
如何解决ABA问题?
- 解决ABA最简单的方案就是给值加一个修改版本号,每次值变化,都会修改它的版本号,CAS操作时都去对比此版本号。
java中ABA解决方法(AtomicStampedReference)
AtomicStampedReference主要包含一个对象引用及一个可以自动更新的整数“stamp”的pair对象来解决ABA问题。
使用AtomicStampedReference修改ABAbug
public class CAS_ABA {
//参数:初始值和初始版本号。 泛型:此引用的对象的类型
public static AtomicStampedReference<Integer> a = new AtomicStampedReference(new Integer(1), 1);
public static void main(String[] args) {
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("操作线程:" + Thread.currentThread().getName() + ",初始值:" + a.getReference());
try {
//public V getReference()返回引用的当前值。
Integer expectReference = a.getReference();
Integer newReference = expectReference + 1;
//public int getStamp()返回stamp(类似版本号)的当前值。
Integer stamp = a.getStamp();
Integer newStamp = stamp + 1;
//主线程休眠1s,让出CPU
Thread.sleep(1000);
/*
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
如果expectedReference==newReference,且expectedStamp==newStamp,则将该值原子设置为给定的更新值。
参数:
expectedReference - 参考的预期值
newReference - 参考的新值
expectedStamp - Stamp的预期值
newStamp - Stamp新值 */
boolean isCASSucceed = a.compareAndSet(expectReference, newReference, stamp, newStamp);//若输出true,则有ABA问题
System.out.println("操作线程:" + Thread.currentThread().getName() + ",CAS操作:" + isCASSucceed + ",a的值为:" + a.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "MAIN").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
//先休眠,确保Thread-MAIN线程优先执行
Thread.sleep(20);
a.compareAndSet(a.getReference(), a.getReference()+1, a.getStamp(), a.getStamp()+1);
System.out.println("操作线程:" + Thread.currentThread().getName() + ",【increment】值:" + a.getReference());
a.compareAndSet(a.getReference(), a.getReference()-1, a.getStamp(), a.getStamp()+1);
System.out.println("操作线程:" + Thread.currentThread().getName() + ",【decrement】值:" + a.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "干扰线程").start();
}
}
CAS总结
任何技术都不是完美的,当然,CAS也有他的缺点:
CAS实际上是一种自旋锁,
-
一直循环,开销比较大。
-
只能保证一个变量的原子操作,多个变量依然要加锁。
-
引出了ABA问题(AtomicStampedReference可解决)。
而他的使用场景适合在一些并发量不高、线程竞争较少的情况,加锁太重。但是一旦线程冲突严重的情况下,循环时间太长,为给CPU带来很大的开销。