【Java】手把手模拟CAS,瞬间理解CAS的机制
- 话不多少,先看个案例,【模拟100个用户,每个用户访问10次网站】”:
public class ThreadDemo1 { //总访问量 private static int count = 0; //模拟访问的方法 public static void request() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(5);//模拟耗时5s 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 < 100; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { //模拟用户行为,每个用户访问10次网站 try { for (int j = 0; j < 10; j++) { request(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { countDownLatch.countDown(); } } }); thread.start(); } countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println("访问用时:" + (endTime - startTime) + "ms"); System.out.println(count); } }
#结果1:
访问用时:73ms 968
#结果2:
访问用时:78ms 983
总之,结果基本都不会达到1000
- 分析一下问题出在哪呢?
【count++】 操作实际上是由三步来完成的(jvm执行引擎)
1》获取count的值,记做A: A = count 2》将A值+1,得到B:B=A+1 3》将B值赋给count
- 怎么解决结果不正确的问题呢?
对count++操作的时候,我们让多个线程排队处理,多个线程同时到达处理【request()方法】时,只能允许有一个线程进去操作,其他线程只能在外面等待(即串行化),
等到里面的线程处理完毕后,再让外面等待的线程进去一个,这样操作结果一定是争取的。
- 通常如何实现排队呢?
1》synchronized关键字加锁
(详情可以了解《Synchronized底层加锁原理详解》:https://www.cnblogs.com/boluopabo/p/12907916.html)
2》ReentrantLock可重入锁
- 那我们试一下【synchronized】加锁后的执行结果:
//我们试着在request方法前加一个synchronized 修饰
//模拟访问的方法 public synchronized static void request() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(5);//模拟耗时5s count++; }
#结果:
访问用时:5884ms 1000
虽然解决了线程的不安全问题,但是用却多了几十倍。
- 耗时太长的原因是什么呢?
程序中的request方法使用synchronized关键字修饰,保证了并发情况下,request方法同一时刻,只允许一个线程进入,
request相当于串行执行了,count结果与预期一致,但耗时太长了
- 如何解决耗时太长问题呢?
文章最初我们简述了【count】的变化过程,这里我们再重复一遍,并延伸一下:
1》获取count的值,记做A: A = count 2》将A值+1,得到B:B=A+1 3》将B值赋给count
升级第三步的实现:
a.获取锁
b.获取以下count最新的值,记做LV
c.判断LV是否等于A,如果相等,则把B的值赋值给count,并返回true;否则返回false
d.释放锁
- 我们这里模拟一下上述升级第三步的实现场景:
package com.example.demo.thread; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * @author Code Farmer * @date 2020/5/22 15:10 */ public class ThreadDemo3 { //总访问量 //此处加volatile修改,保证可见性 private volatile static int count = 0; //模拟访问的方法 public static void request() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(5);//模拟耗时5s // count++; int expectCount; //a.获取锁 //b.获取以下count最新的值,记做LV while (!compareAndSwap((expectCount = getCount()), expectCount + 1)) { } } /** * @param expectCount count期望值 * @param newCount 需要给count赋予的新值 * @return 交换成功返回true;反之返回false */ public static synchronized boolean compareAndSwap(int expectCount, int newCount) { // c.判断LV是否等于A,如果相等,则把B的值赋值给count,并返回true;否则返回false // d.释放锁 if (expectCount == getCount()) { count = newCount; return true; } return false; } private 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 < 100; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { //模拟用户行为,每个用户访问10次网站 try { for (int j = 0; j < 10; j++) { request(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { countDownLatch.countDown(); } } }); thread.start(); } countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println("访问用时:" + (endTime - startTime) + "ms"); System.out.println(count); } }
#结果: 访问用时:78ms 1000
返回结果可以看到,性能可谓是爆炸式提升啊。
- 小结:在模拟CAS机制中,我们只在【将B值赋给count】这里加锁,从而减小了锁的粒度,以提高性能。
学而不思则罔 思而不学则殆 !