这是java高并发系列第21篇文章。java
需求:咱们开发了一个网站,须要对访问量进行统计,用户每次发一次请求,访问量+1,如何实现呢?web
下面咱们来模仿有100我的同时访问,而且每一个人对我们的网站发起10次请求,最后总访问次数应该是1000次。实现访问以下。数据库
代码以下:tomcat
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++操做其实是由如下三步操做完成的:网络
若是有A、B两个线程同时执行count++,他们同时执行到上面步骤的第1步,获得的count是同样的,3步操做完成以后,count只会+1,致使count只加了一次,从而致使结果不许确。多线程
那么咱们应该怎么作的呢?并发
对count++操做的时候,咱们让多个线程排队处理,多个线程同时到达request()方法的时候,只能容许一个线程能够进去操做,其余的线程在外面候着,等里面的处理完毕出来以后,外面等着的再进去一个,这样操做count++就是排队进行的,结果必定是正确的。
咱们前面学了synchronized、ReentrantLock能够对资源加锁,保证并发的正确性,多线程状况下能够保证被锁的资源被串行访问,那么咱们用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秒多。
咱们在看一下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 释放锁
若是咱们发现第3步返回的是false,咱们就再次去获取count,将count赋值给A,对A+1赋值给B,而后再将A、B的值带入到上面的过程当中执行,直到上面的结果返回true为止。
咱们用代码来实现,以下:
package com.itsoku.chat20; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * 跟着阿里p7学并发,微信公众号:javacode2018 */ public class Demo3 { //访问次数 volatile static int count = 0; //模拟访问一次 public static void request() throws InterruptedException { //模拟耗时5毫秒 TimeUnit.MILLISECONDS.sleep(5); int expectCount; do { expectCount = getCount(); } while (!compareAndSwap(expectCount, expectCount + 1)); } /** * 获取count当前的值 * * @return */ public static int getCount() { return count; } /** * @param expectCount 指望count的值 * @param newCount 须要给count赋的新值 * @return */ 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 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,耗时:116,count=1000
代码中用了volatile
关键字修饰了count,能够保证count在多线程状况下的可见性。关于volatile关键字的使用,也是很是很是重要的,前面有讲过,不太了解的朋友能够去看一下:volatile与Java内存模型
我们再看一下代码,compareAndSwap
方法,咱们给起个简称吧叫CAS
,这个方法有什么做用呢?这个方法使用synchronized
修饰了,能保证此方法是线程安全的,多线程状况下此方法是串行执行的。方法由两个参数,expectCount:表示指望的值,newCount:表示要给count设置的新值。方法内部经过getCount()
获取count当前的值,而后与指望的值expectCount比较,若是指望的值和count当前的值一致,则将新值newCount赋值给count。
再看一下request()方法,方法中有个do-while循环,循环内部获取count当前值赋值给了expectCount,循环结束的条件是compareAndSwap
返回true,也就是说若是compareAndSwap若是不成功,循环再次获取count的最新值,而后+1,再次调用compareAndSwap方法,直到compareAndSwap
返回成功为止。
代码中至关于将count++拆分开了,只对最后一步加锁了,减小了锁的范围,此代码的性能是否是比方式2快很多,还能保证结果的正确性。你们是否是感受这个compareAndSwap
方法挺好的,这东西确实很好,java中已经给咱们提供了CAS的操做,功能很是强大,咱们继续向下看。
CAS,compare and swap的缩写,中文翻译成比较并交换。
CAS 操做包含三个操做数 —— 内存位置(V)、预期原值(A)和新值(B)。 若是内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。不然,处理器不作任何操做。不管哪一种状况,它都会在 CAS 指令以前返回该 位置的值。(在 CAS 的一些特殊状况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;若是包含该值,则将 B 放到这个位置;不然,不要更改该位置,只告诉我这个位置如今的值便可。”
一般将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来得到新 值 B,而后使用 CAS 将 V 的值从 A 改成 B。若是 V 处的值还没有同时更改,则 CAS 操做成功。
系统底层进行CAS操做的时候,会判断当前系统是否为多核系统,若是是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功以后会执行cas操做,也就是说CAS的原子性其实是CPU实现的, 其实在这一点上仍是有排他锁的.,只是比起用synchronized, 这里的排他时间要短的多, 因此在多线程状况下性能会比较好。
java中提供了对CAS操做的支持,具体在sun.misc.Unsafe
类中,声明以下:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
上面三个方法都是相似的,主要对4个参数作一下说明。
var1:表示要操做的对象var2:表示要操做对象中属性地址的偏移量
var4:表示须要修改数据的指望的值
var5:表示须要修改成的新值
JUC包中大部分功能都是依靠CAS操做完成的,因此这块也是很是重要的,有关Unsafe类,下篇文章会具体讲解。
synchronized
、ReentrantLock
这种独占锁属于悲观锁,它是在假设须要操做的代码必定会发生冲突的,执行代码的时候先对代码加锁,让其余线程在外面等候排队获取锁。悲观锁若是锁的时间比较长,会致使其余线程一直处于等待状态,像咱们部署的web应用,通常部署在tomcat中,内部经过线程池来处理用户的请求,若是不少请求都处于等待获取锁的状态,可能会耗尽tomcat线程池,从而致使系统没法处理后面的请求,致使服务器处于不可用状态。
除此以外,还有乐观锁,乐观锁的含义就是假设系统没有发生并发冲突,先按无锁方式执行业务,到最后了检查执行业务期间是否有并发致使数据被修改了,若是有并发致使数据被修改了 ,就快速返回失败,这样的操做使系统并发性能更高一些。cas中就使用了这样的操做。
关于乐观锁这块,想必你们在数据库中也有用到过,给你们举个例子,可能之后会用到。
若是大家的网站中有调用支付宝充值接口的,支付宝那边充值成功了会回调商户系统,商户系统接收到请求以后怎么处理呢?假设用户经过支付宝在商户系统中充值100,支付宝那边会从用户帐户中扣除100,商户系统接收到支付宝请求以后应该在商户系统中给用户帐户增长100,而且把订单状态置为成功。
处理过程以下:
开启事务 获取订单信息 if(订单状态==待处理){ 给用户帐户增长100 将订单状态更新为成功 } 返回订单处理成功 提交事务
因为网络等各类问题,可能支付宝回调商户系统的时候,回调超时了,支付宝又发起了一笔回调请求,恰好这2笔请求同时到达上面代码,最终结果是给用户帐户增长了200,这样事情就搞大了,公司蒙受损失,严重点可能让公司就此倒闭了。
那咱们能够用乐观锁来实现,给订单表加个版本号version,要求每次更新订单数据,将版本号+1,那么上面的过程能够改成:
获取订单信息,将version的值赋值给V_A if(订单状态==待处理){ 开启事务 给用户帐户增长100 update影响行数 = update 订单表 set version = version + 1 where id = 订单号 and version = V_A; if(update影响行数==1){ 提交事务 }else{ 回滚事务 } } 返回订单处理成功
上面的update语句至关于咱们说的CAS操做,执行这个update语句的时候,多线程状况下,数据库会对当前订单记录加锁,保证只有一条执行成功,执行成功的,影响行数为1,执行失败的影响行数为0,根据影响行数来决定提交仍是回滚事务。上面操做还有一点是将事务范围缩小了,也提高了系统并发处理的性能。这个知识点但愿大家能get到。
cas这么好用,那么有没有什么问题呢?还真有
ABA问题
CAS须要在操做值的时候检查下值有没有发生变化,若是没有发生变化则更新,可是若是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,可是实际上却变化了。这就是CAS的ABA问题。 常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A
就会变成1A-2B-3A
。 目前在JDK的atomic包里提供了一个类AtomicStampedReference
来解决ABA问题。这个类的compareAndSet方法做用是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
循环时间长开销大
上面咱们说过若是CAS不成功,则会原地循环(自旋操做),若是长时间自旋会给CPU带来很是大的执行开销。并发量比较大的状况下,CAS成功几率可能比较低,可能会重试不少次才会成功。
juc框架中提供了一些原子操做,底层是经过Unsafe类中的cas操做实现的。经过原子操做能够保证数据在并发状况下的正确性。
此处咱们使用java.util.concurrent.atomic.AtomicInteger
类来实现计数器功能,AtomicInteger内部是采用cas操做来保证对int类型数据增减操做在多线程状况下的正确性。
计数器代码以下:
package com.itsoku.chat20; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * 跟着阿里p7学并发,微信公众号:javacode2018 */ public class Demo4 { //访问次数 static AtomicInteger count = new AtomicInteger(); //模拟访问一次 public static void request() throws InterruptedException { //模拟耗时5毫秒 TimeUnit.MILLISECONDS.sleep(5); //对count原子+1 count.incrementAndGet(); } 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,耗时:119,count=1000
耗时很短,而且结果和指望的一致。
关于原子类操做,都位于java.util.concurrent.atomic
包中,下篇文章咱们主要来介绍一下这些经常使用的类及各自的使用场景。
阿里p7一块儿学并发,公众号:路人甲java,天天获取最新文章!