CAS(compare and swap,即比较并交换)
首先,我们引入一个需求。
假设我们开发一个网站,需要对访问量进行统计,用户每发送一次请求,访问量+1,如何实现?
我们模拟有100个人同时访问,并且每个人对咱们的网站发起10次请求,最后总访问的次数应该是1000次。
通过代码进行相关设计:
static int count = 0;
public static void request() throws InterruptedException{
// 模拟耗时5毫毛
TimeUnit.MILLISECONDS.sleep(5);
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++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 模拟用户行为,每个用户访问10次网站
try{
for(int j =0;j<10; j++){
request();
}
}catch (Exception e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
thread.start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"耗时:"+(endTime - startTime)+",count ="+count);
}
通过这段代码执行,看似没问题。我们执行一下,就会发现了。
显然,这个结果并不是我们预期的1000. 那么问题出现在什么地方了呢?
分析如下:
count++这个操作实际上由3步操作完成(具体见jvm相关知识)
1.获取count的值,记做A:A=count
2.将A的值+1,得到B: B=A+1;
3.将B赋值给count; count =B;
如果有A,B两个线程同时执行count++,他们通知执行到上面步骤的第一步,得到的count是一样的,3步操作结束后,count只加1,导致count结果不正确。
那么,我们知道了问题。怎么解决这个不正确的问题呢?
对Count++操作的时候,我们让多个线程排队处理,多个线程同时到达request()方法的时候,只能允许一个线程进去操作,其他线程必须在外面等待,等里面的处理完毕出来之后,外面等着的才可以进去,这样操作,count++就是排队进行的,结果一定正确。
接下来,我们就只需要考虑怎么实现队列效果了。
这个在Java中已经帮我们实现了。synchorinzed关键字和ReentrantLock都可以实现对资源的枷锁,保证并发正确性,多线程的情况下可以保证被锁住的资源被“串行”访问。
在request方法中使用了synchronized关键字修饰,保证了并发情况,request方法同一时刻只允许一个线程进入,request枷锁相当于串行执行了,count的结果和我们预期的一致。
但是呢,这个方式,却耗时太长了。
好了,接下来,我们就处理耗时问题。进行“锁升级”方式处理。
很显然,我们并不需要对整个request()方法进行锁资源,只需要对++这个操作进行锁就够了。
第三步的实现升级为:
1.获取锁;
2.获取以下count的值,记做LV
3.判断LV是否和A相等,如果相等,将B的值赋值给count,并返回true,否则返回false;
4.释放锁
实现如下:
volatile static int count = 0;
public static void request() throws InterruptedException{
// 模拟耗时5毫毛
TimeUnit.MILLISECONDS.sleep(5);
int expectCount; // 表示期望值
while (!compareAndSwap((expectCount = getCount()),expectCount+1)){}
}
public static int getCount(){
return count;
}
public static synchronized boolean compareAndSwap(int expectCount,int newCount){
// 判断count当前值是否与期望值expectCount一致,如果一致,将newCount赋值给count
if(getCount() == expectCount){
count = newCount;
return true;
}
return false;
}
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++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 模拟用户行为,每个用户访问10次网站
try{
for(int j =0;j<10; j++){
request();
}
}catch (Exception e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
thread.start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName()+"耗时:"+(endTime - startTime)+",count ="+count);
}
这样,如图,我们就可以很快的获取到一个正确的值了。
这里用到了CAS(compare and swap)原理
CAS原理:
CAS通过调用JNI的代码实现的吗,JNI:Java Native Interface,允许Java调用其他语言。而compareAndSwapxx系列的方法就是借助“C语言”来调用cpu底层指令实现的。
源码中,如下所示:
那么CAS是万能的么?没有任何问题了么?
不是的,CAS在使用的时候,会遇到ABA问题。
什么是ABA问题?
CAS需要在操作值的时候检查下值是否发生变化,如果没有发生变化,才更新。但是如果一个值原来是A,在CAS方法执行之前,被其他线程修改为了B,然后又修改回了A,那么CAS方法执行检查的时候会发现它的值没有发生改变,但实际上却变化了。这就是CAS的ABA问题。
关于如何解决ABA问题。下次进行详细的学习记录。