场景模拟
假设商品有500件库存,进行促销预购,每有一位客户预购,商品预购数加1。
省略数据库的操作,用i++来模拟数据库操作
正常JAVA实现
public class CASTest {
public static int numValue;//商品预购数
public static void main(String[] args) throws InterruptedException {
CASTest test = new CASTest();
for (int i = 0; i < 500; i++) {
new Thread(() -> { // 起500个线程,当成500个客户同时都在预购该商品
try {
Thread.sleep(30);// 模拟操作延时
} catch (InterruptedException e) {
e.printStackTrace();
}
test.addNum();
}).start();
}
Thread.sleep(3000);// 等上面线程都跑完后再看 商品预购数 的值
System.out.println("numValue:" + test.numValue);
}
//模拟是预购数加1操作
public void addNum() {
numValue++;//模拟数据库操作
System.out.println(numValue);
}
}
预期结果:最后输出的 numValue 值应该是500
实际结果:
每次有客户预购后numValue的输出是无序的,结果可能不是500
原因:numValue++ 时,会从内存中读取数值numValue,然后numValue+1,最后再赋值回numValue。 当并发量大时,多个用户同时读取到相同的numValue值,各自+1,再赋值回numValue,导致numValue原本应该加2或者加3最后却只加了1. 显然这样的写法会导致数据一致性错误
JAVA加锁实现
写法与上面相同,只是addNum() 方法加个锁 变成了
//模拟是预购数加1操作
public synchronized void addNum() {
numValue++;//模拟数据库操作
System.out.println(numValue);
}
预期结果:最后输出的 numValue 值应该是500
实际结果:
每次有客户预购后numValue的输出是有序的,结果是500
显然加锁可以避免并发操作时出现的数据不一致问题,但是每次只能一个客户去进行预购操作,效率低下并且如果中间有客户操作时出现阻塞后面的客户也会受影响,即使用lock锁,实际情况下也不建议使用
JAVA使用CAS实现
CAS算法:CAS是CPU的指令,是属于硬件层次的,底层也用了锁,相当于是硬件层次上的处理并发修改变量的操作算法,有三个操作数,内存地址V ,预期值B(就是你获取的numValue的修改前的值),要替换得到的目标值A(修改后的numValue值)。
CAS指令执行时,比较内存地址V(内存地址的numValue值)与预期值B是否相等,若相等则将A赋给B,否则不赋值,这时候我们可以通过自旋去重新去获取期望值,重新再去进行CAS操作。
我们只需要知道JAVA怎么去调用和传参,具体的更新操作交给CAS算法
具体写法可以参考AtomicInteger.class
import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class CASTest2 {
private volatile int numValue;//无阻塞修改的参数
private static Unsafe unsafe;
private static final long valueOffset;//相当于CAS的获取内存地址
static {
try {
//初始化 unsafe
Field f;
f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
//初始化 valueOffset
valueOffset = unsafe.objectFieldOffset(CASTest2.class.getDeclaredField("numValue"));
} catch (Exception ex) {
throw new Error(ex);
}
}
public static void main(String[] args) throws Exception {
CASTest2 test = new CASTest2();
for (int i = 0; i < 500; i++) {
new Thread(() -> { // 起500个线程,当成500个用户都在修改同一个数据
try {
Thread.sleep(30);// 模拟操作延时
} catch (InterruptedException e) {
e.printStackTrace();
}
test.addNum();
System.out.println(test.numValue);
}).start();
}
Thread.sleep(3000);// 等上面线程都跑完后再看 numValue 的值
System.out.println("numValue:" + test.numValue);
}
public final int get() {
return numValue;
}
public final int addNum() {
for (;;) {
//如果竞争失败,重新获取期望值,重新更新,直到成功。实际情况就是用select 去查库重新获取新的期望值
int current = get();//期望值
int next = current + 1;//目标值
if (compareAndSet(current, next)) {
//下面可以加数据库更新操作
return next;
} else {//当多个客户同时修改 numValue时,只有一个人可以操作成功,失败的人就会进入到下面的分支,重新再去更新操作
System.out.println("当current=" + current + " 时有竞争并竞争失败,自旋一次重新自增");
}
}
}
public final boolean compareAndSet(int expect, int update) {
//unsafe会调用java公共组件,再调C++代码再调底层CAS算法,我们不用管,值的修改也交给下面的方法
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
预期结果:最后输出的 numValue 值应该是500
实际结果:
每次有客户预购后numValue的输出是无序的,结果是500,并且也会发现会后竞争失败,重新去更新的操作。虽然也会出现竞争的情况,却可以通过CAS算法,避免了java层面上的加锁
总结:
上述被并发修改的参数是int类型的,实际上使用别的类型的参数也是可以操作的。但是这个使用cas算法实现的并发修改参数只能保证一个变量,如果同时修改多个参数的情况下还需要进行代码优化,并且如果并发量过大,会导致自旋的次数大大增加,压力在服务器这边,要根据实际情况去判断是否要采用这种操作。