java高并发系列 - 第21天:java中的CAS操作,java并发的基石

从网站计数器实现中一步步引出CAS操作

介绍java中的CAS及CAS可能存在的问题

悲观锁和乐观锁的一些介绍及数据库乐观锁的一个常见示例

使用java中的原子操作实现网站计数器功能

我们需要解决的问题

需求:我们开发了一个网站,需要对访问量进行统计,用户每次发一次请求,访问量+1,如何实现呢?

下面我们来模仿有100个人同时访问,并且每个人对咱们的网站发起10次请求,最后总访问次数应该是1000次。实现访问如下。

方式1

代码如下:

package com.itsoku.chat20; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * 跟着阿里p7学并发,微信公众号:javacode2018 */ public class Demo1 { //访问次数 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 starTime = System.currentTimeMillis(); int threadSize = 100; CountDownLatch countDownLatch = new CountDownLatch(threadSize); for (int i = 0; i < threadSize; i++) { Thread thread = new Thread(() -> { 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(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count); } }

输出:

main,耗时:138,count=975

代码中的count用来记录总访问次数,request()方法表示访问一次,内部休眠5毫秒模拟内部耗时,request方法内部对count++操作。程序最终耗时1秒多,执行还是挺快的,但是count和我们期望的结果不一致,我们期望的是1000,实际输出的是973(每次运行结果可能都不一样)。

分析一下问题出在哪呢?

代码中采用的是多线程的方式来操作count,count++会有线程安全问题,count++操作实际上是由以下三步操作完成的:

获取count的值,记做A:A=count

将A的值+1,得到B:B = A+1

让B赋值给count:count = B

如果有A、B两个线程同时执行count++,他们同时执行到上面步骤的第1步,得到的count是一样的,3步操作完成之后,count只会+1,导致count只加了一次,从而导致结果不准确。

那么我们应该怎么做的呢?

对count++操作的时候,我们让多个线程排队处理,多个线程同时到达request()方法的时候,只能允许一个线程可以进去操作,其他的线程在外面候着,等里面的处理完毕出来之后,外面等着的再进去一个,这样操作count++就是排队进行的,结果一定是正确的。

我们前面学了synchronized、ReentrantLock可以对资源加锁,保证并发的正确性,多线程情况下可以保证被锁的资源被串行访问,那么我们用synchronized来实现一下。

使用synchronized实现

代码如下:

package com.itsoku.chat20; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * 跟着阿里p7学并发,微信公众号:javacode2018 */ public class Demo2 { //访问次数 static int count = 0; //模拟访问一次 public static synchronized void request() throws InterruptedException { //模拟耗时5毫秒 TimeUnit.MILLISECONDS.sleep(5); count++; } public static void main(String[] args) throws InterruptedException { long starTime = System.currentTimeMillis(); int threadSize = 100; CountDownLatch countDownLatch = new CountDownLatch(threadSize); for (int i = 0; i < threadSize; i++) { Thread thread = new Thread(() -> { 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(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count); } }

输出:

main,耗时:5563,count=1000

程序中request方法使用synchronized关键字,保证了并发情况下,request方法同一时刻只允许一个线程访问,request加锁了相当于串行执行了,count的结果和我们预期的结果一致,只是耗时比较长,5秒多。

方式3

我们在看一下count++操作,count++操作实际上是被拆分为3步骤执行:

1. 获取count的值,记做A:A=count 2. 将A的值+1,得到B:B = A+1 3. 让B赋值给count:count = B

方式2中我们通过加锁的方式让上面3步骤同时只能被一个线程操作,从而保证结果的正确性。

我们是否可以只在第3步加锁,减少加锁的范围,对第3步做以下处理:

获取锁 第三步获取一下count最新的值,记做LV 判断LV是否等于A,如果相等,则将B的值赋给count,并返回true,否者返回false 释放锁

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwszsx.html