首先我们来看一段代码(模拟抢货):
图片.png代码解释:下面代码模拟并发抢货过程,我们用了6个线程处理6万的货,按理来说应该是刚刚好库存全部为0.
public class RushOrder { //库存类
//我们库存总共6万
int i =60000;
public void order(){
i--;
}
//模拟高并发状态下的抢货业务场景
public static void main(String[] args) throws InterruptedException {
RushOrder rushOrder =new RushOrder();
for (int j = 0; j <6; j++) {
//多个线程 6个线程一块去处理
new Thread(
()->{ //JDK1.8 的表达式 代表的就是run方法
//每个线程走一万次
for (int k = 0; k <10000 ; k++) {
rushOrder.order();
}
}
).start();
}
//休眠5秒
Thread.sleep(5000);
System.out.println("当前库存"+rushOrder.i);
}
}
上面的 代码执行结果很显然并不是我们想要的执行结果
第一次测试:当前库存20533
第二次测试:当前库存26500
那这个i--操作在jvm中是怎么走的呢?
mysql.png
图解:
这里的i--执行了三部操作,那为什么最终的值会不是0呢?原因很简单,因为i--它没有保证原子性
那什么是原子性呢?
- 一个或多个操作时,要不全部执行,或者在一个线程执行过程中不被任何因素打乱,要不全部不执行.
那为什么i--就不满足原子性呢?(看下图)
mysql.png图解:
当线程1拿到i=10的值的时候,执行i--的操作时突然线程二就进来了,抢占了线程的执行权
线程2在共享变量拿到的值也是10,执行i--;然后最终来个线程执行完成后,put上去的值都是9,这就造成了为什么为什么每次执行货不是0的情况.
解决方案
- 使用原子操作类:
Atomiclnteger :原子操作类
更改代码:
//我们库存总共6万
/*inti=60000;*/
AtomicIntegeri=newAtomicInteger(60000);//相当于i=60000但是它是一个原子操作类
publicvoidorder(){
//i--;
i.decrementAndGet(); //等同于i--它是一个原子性的
}
最终的执行结果为:当前库存0
那为什么decrementAndGet()就能保住线程安全呢?
-
其实底层原理就是CAS(Compare and swap) 比较和交换.(见下图)
mysql.png
图解:
- 比如当前我们的主内存的值是10,我们的线程1来到我们的工作区拿到我们的旧值old=10,
然后进行CAS比较与主内存的值是否一致,如果不一致的话我们就重新回到get(i),old重新去主内存中拿值,再进行比较 如果这时一比较发现相等,我们就可以吧i-1的值赋给主内存,
就解决了最后货的值为0的问题.
为什么我们遇到这类原子性问题不去经常选择使用原子操作类,而去使用锁呢?
CAS机制的三大问题:
mysql.png图解:
-
1.只能确保一个共享变量的原子性操作
我们来看调用的方法:
i.decrementAndGet();//等同于i--它是一个原子性的
public final int decrementAndGet(){
return unsafe.getAndAddInt(this,valueOffset,-1)-1;
}
如果我们一旦遇到复杂的运算,比如i=i*10/6 就还得每次就定义方法. -
2.循环时间长,开销大(这里jvm实现的sync锁会升级为重量级锁)
每次CAS机制如果都失败了,它必定每次都去重试.
它是一种乐观机制,它认为总是能匹配对的值,
但是一直失败会一直匹配一直循环的话会造成cpu占用太长. -
3.ABA问题: (脏读,脏取)
ABA对象, 比如A(未婚),B(已婚)
比如当我们的老王在放暑假前通过朋友介绍了一个妹子她未婚(A) old=A ,但是老王出去打暑假工了,妹子可能有别的追求者,然后别的线程进来跟她嘿嘿嘿,于是他们就结婚了,主内存的妹子就变成了(B),但是一个月内他们闹矛盾,男的时间太短,离婚了,妹子又单身了,于是呢老王打工回来了,这是CAS匹配主内存发现未婚(A),这时老王就和她结婚了.
网友评论