JAVA并发编程与高并发解决方案 - 并发编程 一 之 并发相关知识

JAVA并发编程与高并发解决方案 - 并发编程 一

版本 作者 内容
2018.5.6 chuIllusions 首次发布
2018.5.29 chuIllusions 修改部分内容

学习笔记说明

本系列文章,是通过学习慕课网中Java并发编程与高并发解决方案整理的笔记,对课程的知识点进行补充。同时,感谢该课程老师所带来的学习内容,让我深入学习了好多知识点。

相关文章

JAVA并发编程与高并发解决方案 - 并发编程 二 之 线程安全性、安全发布对象
JAVA并发编程与高并发解决方案 - 并发编程 三 之 线程安全策略
JAVA并发编程与高并发解决方案 - 并发编程 四 之 J.U.C之AQS
JAVA并发编程与高并发解决方案 - 并发编程 五 之 J.U.C组件拓展
JAVA并发编程与高并发解决方案 - 并发编程 六 之 线程池

学习内容简介

并发编程知识点

线程安全、线程封闭、线程调度、同步容器、并发容器、AQS、J.U.C etc.

高并发解决方案知识点

扩容、缓存、队列、拆分、服务降级与熔断、数据库切库、分库分表 etc.

面对人群

从事JAVA开发的程序员

  1. 对并发和高并发不了解的程序员
  2. 对并发和高并发了解的程序员
  3. 已经是编程高手的程序员

目的

构建完整的并发与高并发知识体系

  1. 系统的学习到并发编程的知识及高并发处理思路
  2. 修正之前在不知不觉中犯过的一些并发方面的问题
  3. 规避以后开发中一些并发方面的问题
  4. 对你的知识进行一次更为全面的梳理,完善知识体系
  5. 学习到大量的实际场景案例分析和代码优化技巧
  6. 让你对并发编程和高并发处理有一个质的提升
  7. 将节省你准备面试的时间,让你的面试更有针对性
  8. 可以借鉴一些之前可能没有想到过的解决问题思路和手段

课程内容安排

基础知识讲解与核心知识准备

image

并发及并发的线程安全处理

image

高并发处理的思路及手段

image

涉及知识技能

总体架构:Spring Boot、Maven、JDK8、MySQL

基础组件:Mybatis、Guava、Lombok、Redis、Kafka

高级组件(类):Joda-Time、Atomic包、J.U.C、AQS、ThreadLocal、RateLimiter、Hystrix、ThreadPool、shardbatis、curator、elastic-job ...

场景举例 - 实现计数功能

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
public class CountExample1 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    //5000个请求,每次只允许200个请求处理

    public static int count = 0;

    public static void main(String[] args) throws Exception {
        //线程池 + 信号量 进行请求的模拟
        //新建线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量(后面会进行讲解)
        final Semaphore semaphore = new Semaphore(threadTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        count++;
    }
}

点击运行结果,每一次结果都是不一样的,并且没有达到结果为5000,而是小于5000

@Slf4j
public class HashMapExample {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static Map<Integer, Integer> map = new HashMap<>();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    //每次允许threadTotal请求进行处理
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        executorService.shutdown();
        log.info("size:{}", map.size());
    }

    private static void update(int i) {
        map.put(i, i);
    }
}

点击运行结果,每一次结果都是不一样的,并且Map.size()没有达到结果为5000,而是小于5000

若上面两个例子中,threadTotal = 1 则会得到我们预期的结果,size() = clientTotal = 5000

总结:当一个线程运行可以得到我们预期的结果,但当多个线程同时进行操作,就会出现并发问题,导致结果异常

@Slf4j

slf4j

对于一个maven项目。首先要在pom.xml中加入以下依赖项:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.5</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.5</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
  1. slf4j就是众多接口的集合,它不负责具体的日志实现,只在编译时负责寻找合适的日志系统进行绑定。具体有哪些接口,全部都定义在slf4j-api中。
  2. slf4j-log4j12是链接slf4j-api和log4j中间的适配器。它实现了slf4j-apiz中StaticLoggerBinder接口,从而使得在编译时绑定的是slf4j-log4j12的getSingleton()方法
  3. log4j是具体的日志系统。通过slf4j-log4j12初始化Log4j,达到最终日志的输出。
  4. lombok:一个插件,封装了log的get和set,可以直接使用log来输出日志信息。

@slf4j

如果不想每次都写private final Logger logger = LoggerFactory.getLogger(XXX.class); 可以用注解@Slf4j

引入依赖,使用方式如上场景举例中代码示例

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.10</version>
</dependency>

解决IDEA使用@Slf4j注入后找不到变量log

方式一:

idea中File --> settings --> Plugins --> 点击"Browse repositories" --> 搜索lombok --> Install Lombok Plugins

若插件安装失败,则可以进行以下安装方式

方式二:

去idea官网下载插件 Lombok Plugin ,到下载区,选择合适的版本下载,我的idea版本为2017.1.4,因此选择插件的版本号为0.16-2017.1.4 下载到文件为lombok-plugin-0.16.zip

注:idea任何插件的版本都需要跟idea版本对应,否则会提示安装失败(本人踩过的坑)

安装步骤:解压下载到的zip文件,拷贝解压文件到idea安装目录下的plugins文件下,打开idea中的 plugins > 选择 install plugin from disk > 选择刚刚拷贝进去的文件夹中的jar,即可进行安装,安装完成后需要进行重启。

Lombox

简介:

Lombok项目是一个java库,可以自动插入到您的编辑器和构建工具中,让您的java变得更加精彩。切勿再次写入另一个getter或equals方法。提前访问未来的Java功能val,等等。

除了官方介绍中,并不多相关文章,特意挑了一篇文章中相关内容

lombok 提供了简单的注解的形式来帮助我们简化消除一些必须有但显得很臃肿的 java 代码。特别是相对于 POJO。
简单来说,比如我们新建了一个类,然后在其中写了几个字段,然后通常情况下我们需要手动去建立getter和setter方法啊,构造函数啊之类的,lombok的作用就是为了省去我们手动创建这些代码的麻烦,它能够在我们编译源码的时候自动帮我们生成这些方法。

lombok能够达到的效果就是在源码中不需要写一些通用的方法,但是在编译生成的字节码文件中会帮我们生成这些方法,这就是lombok的神奇作用。

虽然有人可能会说IDE里面都自带自动生成这些方法的功能,但是使用lombok会使你的代码看起来更加简洁,写起来也更加方便。

常用的注解

@slf4j、@Setter、@Getter、@NoArgsConstructor(注解在类上:为类提供一个无参的构造方法)、@AllArgsConstructor(注解在类上;为类提供一个全参的构造方法)

@NoArgsConstructor //注解在类上:为类提供一个无参的构造方法
@AllArgsConstructor//注解在类上;为类提供一个全参的构造方法
public class Person {
    //@Getter @Setter 注解在属性上;为属性提供 setting 方法 getting方法
    @Setter @Getter private int pid;
    @Setter @Getter private String pname;
    @Setter @Getter private int sage;
}

基础知识讲解与核心知识准备

并发与高并发基本概念

概念

  并发:同时拥有两个或者多个线程,如果程序在单核处理器运行,多个线程将交替地换入或者换出内存,这些线程是同时"存在"的,每个线程都处于执行过程中的某个状态,如果运行在多核处理器上,此时,程序中的每个线程都将会分配到一个处理器核上,因此可以同时运行

  并行:系统中有多个任务同时存在可称之为“并发”,系统内有多个任务同时执行可称之为“并行”;并发是并行的子集。如果说并发就是在一台处理器上"同时"处理多个任务,那么并行就是在多台处理器上同时处理多个任务;个人理解是,在单核CPU系统上,并行是无法实现的,只可能存在并发而不可能存在并行。

  高并发:高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常指,通过设计保证系统能够同时并行处理很多请求。

对比:

  并发:多个线程操作相同的资源,保证线程安全,合理使用资源

  高并发:服务能同时处理很多请求,提高程序性能;如系统集中收到大量的请求(12306的抢票系统),导致系统在某段时间类执行大量的操作,包括对资源的请求、数据库的操作等等,如果高并发处理不好,不仅仅降低用户的体验度,请求时间变长,同时也可能导致系统宕机,甚至导致OOM(Out Of Memory)异常,如果想要系统适应高并发状态,就要有多个方面进行系统优化,包括硬件、网络、系统架构、开发语言的选取、数据结构的应用、算法的优化等等,这个时候谈论的是如何提供现有程序的性能,对高并发场景提供一些解决方案、手段等等

CPU多级缓存

  在多线程并发环境下,如果不采取特殊手段,普通的累加结果很可能是错的。错的原因可能涉及到计算机原理以及JAVA方面的一些知识。

介绍

image

Main Memory : 主存

Cache : 高速缓存,数据的读取和存储都经过此高速缓存

CPU Core : CPU核心

Bus : 系统总线

  CUP Core 与 Cache 之间有一条快速通道,Main Memory 与 Cache 关联在 Bus 上,同时 Bus 还用于其他组件 的通信,在Cache出现不久后,系统变得更加复杂,Cache与Main Memory中速度的差异拉大,直到加入另一级的Cache,新加入的Cache 比 一级 Cache 更大,但是更慢,由于从加大一级Cache的做法,从经济上是行不通的,所以有了二级Cache,甚至已经有三级 Cache

为什么需要CPU CACHE?

  CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源,这样会使CPU花费很长时间等待数据到来或把数据写入内存。所以Cache的出现,是为了缓解CPU和内存之间速度的不匹配问题(结构:CPU - > CACHE - > MEMORY)

CPU CACHE 意义

  缓存的容量远远小于主存,因此出现缓存不命中的情况在所难免,既然缓存不能包含CPU所需要的所有数据,那么Cache的存在真的有意义吗?

CPU缓存存在的意义分两点(局部性原理):

  1. 时间局部性:如果某个数据被访问,那么在不久的将来它很可能被再次访问
  2. 空间局限性:如果某个数据被访问,那么与它相邻的数据很快也可能被访问

  缓存的工作原理是当CPU要读取一个数据时,首先从缓存中查找,如果找到就立即读取并运送给CPU处理;如果没有找到,就用相对慢的速度内存中读取并运送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。

  正是这样的读取机制使CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在缓存中,大约10%需要从内存读取。

缓存一致性(MESI)

  缓存一致性用于保证多个CPU Cache之间缓存共享数据的一致性,定义了Cache Line四种状态,而CPU对Cache的四种操作,可能会产生不一致的状态,因此缓存控制器监听到本地操作和远程操作的时候 ,需要对Cache Line作出相应的修改,从而保证数据在多个缓存之间的一致性

  Cache Line : 是cache与内存数据交换的最小单位,根据操作系统一般是32byte或64byte。在MESI协议中,状态可以是M、E、S、I,地址则是cache line中映射的内存地址,数据则是从内存中读取的数据。

  MESI其实是四种状态的缩写:M(modify)修改、E(exclusive)独占、S(shared)共享、I(invalid)失效。

状态间的相互转换关系:

M E S I
M × × ×
E × × ×
S × ×
I

  Cache 操作: MESI协议中,每个cache的控制器不仅知道自己的操作(local read和local write),通过监听也知道其他CPU中cache的操作(remote read和remote write)。对于自己本地缓存有的数据,CPU仅需要发起local操作,否则发起remote操作,从主存中读取数据,cache控制器通过总线监听,仅能够知道其他CPU发起的remote操作,但是如果local操作会导致数据不一致性,cache控制器会通知其他CPU的cache控制器修改状态。

参考文章:【并发编程】CPU cache结构和缓存一致性(MESI协议)

乱序执行优化

  处理器为提高运算速度而做出违背代码原有顺序的优化

举个例子:

  1. 计算 a * b ,a =10 ,b = 200 ,则 result = a * b = 2000
  2. 代码编写顺序:a=10 -> b=200 -> result = a * b
  3. CPU乱序执行优化可能会发生执行顺序为:b=200 -> a=10 -> result = a * b

  CPU乱序执行优化不会对结果造成影响,在单核时代,处理器保证做出的优化,不会导致执行的结果远离预期的目标,但是在多核环境下并非如此。首先在多核环境中,同时会有多个核执行指令,每个核的指定都可能会被乱序优化,另外,处理器还引用了L1、L2等缓存机制,每个核都有自己的缓存,这就导致了逻辑次序上后写入内存的数据,未必真的最后写入,最终带来了这样的一个问题:如果我们不做任何防护措施,处理器最终得到的结果和我们逻辑得出的结果大不相同。比如我们在其中的一个核中执行数据写入操作,并在最后写一个标记,用来标记数据已经准备好了,然后从另外一个核上,通过那个标志,来判断数据是否已经就绪,这种做法它就存在一定的风险,标记位先被写入,但数据操作并未完成(可能是计算为完成、也可能是数据没有从缓存刷新到主存当中), 最终导致另外的核使用了错误的数据。

Java 内存模型(Java Memory Model,JMM)

  CPU缓存一致性和乱序执行优化,在多核多并发下,需要额外做很多的事情,才能保证程序的执行,符合我们的预期。那么JVM(Java Virtual Machine (Java虚拟机))是如何解决这些问题的?为了屏蔽掉各种硬件和操作系统的内存访问差异,实现让Java程序在各种平台下都能达到一致的并发效果,JMV规范中定义了JMM (Java Memory Model (Java 内存模型))。 JMM是一种规范,它规范了JVM与计算机内存是如何协同工作的,它规定一个线程如何和何时可以看到其他线程修改过的共享变量的值,以及在必须时如何同步的访问共享变量。

JVM内存分配概念

图 JVM内存分配概念

JVM内存分配的两个概念:Stack(栈)和Heap(堆)。

  Java中的Heap是运行时数据区,由垃圾回收负责,它的优势是动态的分配内存大小,生存期也不必事先告诉编译器,在运行时动态分配内存,Java的垃圾收集器,会自动回收不再使用的数据。但是也有缺点,由于是要在运行时动态分配内存,因此存取速度相对较慢。

  Java中的Stack优势是存取速度比Heap要快,仅次于计算机中的寄存器,栈中的数据是可以共享的,但是它的缺点是,存在栈中数据的大小和生存期必须是确定的,缺乏灵活性,主要存放一些基本类型的变量。

  JMM要求调用栈和本地变量存放在线程栈中,对象存放在堆上。一个本地变量可能指向一个对象的引用,引用这个本地变量是存放在线程栈上,而对象本身是存放在堆上的。一个对象可能包含方法,这些方法可能包含本地变量,这些本地变量还是存放在线程栈中,即使这些方法所属的对象存放在堆上。一个对象的成员变量可能会随着这个对象自身存放在堆上,不管这个成员对象是原始类型还是引用类型,静态成员变量跟随着类的定义一起存放在堆上。存放在堆上的对象,可以被所持有对这个对象引用线程的访问。

  当一个线程可以访问一个对象的时候,它也可以访问该对象的成员变量,如果两个线程同时调用同一个对象的同一个方法,将会都访问该对象的成员变量,但是每一个线程都拥有了这个成员变量的私有拷贝。

计算机内存硬件架构

计算机硬件架构简单图

  CPU,一台现代计算机拥有两个或多个CPU,其中一些CPU还有多核,从这一点可以看出,在一个有两个或多个CPU的现代计算机上,同时运行多个线程是非常有可能的,而且每个CPU在某一个时刻,运行一个线程是肯定没有问题的,这意味着,如果Java程序是多线程的,在Java程序中,每个CPU上一个线程是可能同时并发执行的。

  CPU Refisters(寄存器),每个CPU都包含一系列的寄存器,它们是CPU内存的基础,CPU在寄存器中执行操作的速度远大于在主存上执行的速度,这是因为CPU访问寄存器的速度远大于主存。

  Cache(高速缓存),由于计算机的存储设备与处理器运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高级缓存来作为内存与处理器之间的缓冲,将运算需要使用到的数据复制到缓存中,让运算能快速的进行,当运算结束后,在从缓存同步到内存中。这样处理器就无需等待缓慢的内存读写,CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度要慢。

  Main Memory(主存),随机存取存储器(random access memory,RAM)又称作“随机存储器",一个计算机包含一个主存,所有的CPU都可以访问主存,主存通常比CPU中的缓存大得多。

JVM 与 Computer

图 JVM与Computer

​ JVM 与 Computer 内存架构存在差异,硬件内存并无区分栈与堆,对于硬件而言,所有的栈和堆都分布在主内存中,可能会出现在高速缓存、寄存器中。

内存模型抽象结构

图 内存模型抽象结构

Java内存模型 - 同步八种操作

  1. lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态
  2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值存放工作内存的变量副本中
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传递到主内存中,以便随后的write的操作
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中

Java内存模型 - 同步规则

  1. 如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作,但Java内存模型只要求上述操作必须按顺序执行,而没有保证是连续执行
  2. 不允许read和load、store和write操作之一单独出现
  3. 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中
  4. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
  5. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作
  6. 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次与执行lock后,只有执行相同次数的unlock,变量才会被解锁。lock和unlock必须成对出现
  7. 如果一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  8. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量
  9. 对一个变量执行unlock操作之前,必须先把变量同步到主内存中(执行store和write操作)

Java 内存模型 - 同步操作与规则

图 同步操作与规则

并发的优势与风险

图 并发优势与风险

并发编程与线程安全

  代码所在的进程,有多个线程同时运行,而这些线程可能会同时运行同一段代码,如果每次运行结果和单线程预期结果一致,变量值也和预期一致,则认为这是线程安全的。简单的说,就是并发环境下,得到我们期望正确的结果。对应的一个概念就是线程不安全,就是不提供数据访问保护,有可能出现多个线程,先后更改数据,造成所得到的数据是脏数据,也可能是计算错误。

环境搭建准备

项目架构

Spring Boot 项目,https://start.spring.io

自定义注解

​ 为方便理解,自定义一些注解,方便理解。

/**
 * 课程里用来标记【线程安全】的类或者写法
 */
@Target(ElementType.TYPE) //作用域,作用于类上
@Retention(RetentionPolicy.SOURCE) //注解存在的范围,编译时忽略
public @interface ThreadSafe {

    //给默认值,方便扩展
    String value() default "";
}
/**
 * 课程里用来标记【线程不安全】的类或者写法
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface NotThreadSafe {

    String value() default "";
}
/**
 * 课程里用来标记【推荐】的类或者写法
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Recommend {

    String value() default "";
}
/**
 * 课程里用来标记【不推荐】的类或者写法
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface NotRecommend {

    String value() default "";
}

并发模拟

模拟准备工具介绍

  1. Postman:Http请求模拟工具
  2. Apache Bench(AB):Apache附带的工具,测试网站性能
  3. JMeter:Apache组织开发的压力测试工具
  4. 代码模拟:Semaphore、CountDownLatch等

服务准备

@RequestMapping("/test")
@ResponseBody
public String test() {
  return "test";
}

Postman

​ Postman本身是一个Http模拟工具,在并发上并不是专业的

使用步骤:

  1. 打开Postman访问localhost:8080/test,完成一次服务访问
  2. 找到Collections标签,新建concurrency文件夹,将刚访问的连接Save文件夹中,并点击文件夹进入测试准备
  3. 配置参数,点击Run Concurrency,成功后查看结果
图 Postman运行并发模拟
图 Postman并发参数配置
图 Postman并发结果

Apache Bench(AB)

  Apache Bench 是 Apache 服务器自带的一个web压力测试工具,简称ab。ab又是一个命令行工具,对发起负载的本机要求很低,根据ab命令可以创建很多的并发访问线程,模拟多个访问者同时对某一URL地址进行访问,因此可以用来测试目标服务器的负载压力。总的来说ab工具小巧简单,上手学习较快,可以提供需要的基本性能指标,但是没有图形化结果,不能监控。

Windows 7 安装
  1. 首先需要安装Apache服务器,点击下载
  2. 将下载httpd-2.4.33-win64-VC15.zip解压
  3. 配置环境变量,这里为了方便,我没有配置,直接进入bin目录,运行控制台
  4. 输入ab命名,若出现以下提示则环境准备成功
D:\apache\Apache24\bin>ab
ab: wrong number of arguments
Usage: ab [options] [http://]hostname[:port]/path
Options are:
    -n requests     Number of requests to perform
    -c concurrency  Number of multiple requests to make at a time
    -t timelimit    Seconds to max. to spend on benchmarking
                    This implies -n 50000
    -s timeout      Seconds to max. wait for each response
                    Default is 30 seconds
    -b windowsize   Size of TCP send/receive buffer, in bytes
    -B address      Address to bind to when making outgoing connections
    -p postfile     File containing data to POST. Remember also to set -T
    -u putfile      File containing data to PUT. Remember also to set -T
    -T content-type Content-type header to use for POST/PUT data, eg.
                    'application/x-www-form-urlencoded'
                    Default is 'text/plain'
    -v verbosity    How much troubleshooting info to print
    -w              Print out results in HTML tables
    -i              Use HEAD instead of GET
    -x attributes   String to insert as table attributes
    -y attributes   String to insert as tr attributes
    -z attributes   String to insert as td or th attributes
    -C attribute    Add cookie, eg. 'Apache=1234'. (repeatable)
    -H attribute    Add Arbitrary header line, eg. 'Accept-Encoding: gzip'
                    Inserted after all normal header lines. (repeatable)
    -A attribute    Add Basic WWW Authentication, the attributes
                    are a colon separated username and password.
    -P attribute    Add Basic Proxy Authentication, the attributes
                    are a colon separated username and password.
    -X proxy:port   Proxyserver and port number to use
    -V              Print version number and exit
    -k              Use HTTP KeepAlive feature
    -d              Do not show percentiles served table.
    -S              Do not show confidence estimators and warnings.
    -q              Do not show progress when doing more than 150 requests
    -l              Accept variable document length (use this for dynamic pages)
    -g filename     Output collected data to gnuplot format file.
    -e filename     Output CSV file with percentages served
    -r              Don't exit on socket receive errors.
    -m method       Method name
    -h              Display usage information (this message)

提示:若启动ab.exe时候,提示缺少某种依赖库,则需要安装该依赖库才可进行启动

运行演示

运行命令:ab -n 1000 -c 50 http://localhost:8080/test

命令解析:-n 请求总次数 -c 并发数 URL地址

D:\apache\Apache24\bin>ab -n 1000 -c 50 http://localhost:8080/test
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname:        localhost
Server Port:            8080

Document Path:          /test
Document Length:        4 bytes

Concurrency Level:      50                 # 并发量
Time taken for tests:   0.834 seconds      # 整个测试使用的时间
Complete requests:      1000               # 完成请求数
Failed requests:        0                  # 失败请求数
Total transferred:      136000 bytes       # 所有请求响应数据的总和(包括Http 头信息和正文数据长度,服务器流向应用层数据总长度)
HTML transferred:       4000 bytes   # 所有响应数据,正文数据总和
Requests per second:    1198.97 [#/sec] (mean) # 吞吐率,与并发数相关
Time per request:       41.702 [ms] (mean) # 用户平均请求等待时间
Time per request:       0.834 [ms] (mean, across all concurrent requests) # 服务器平均请求等待时间
Transfer rate:          159.24 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.8      0      24
Processing:     1   39  87.7     14     584
Waiting:        0   31  75.8     11     558
Total:          2   39  87.7     14     584

Percentage of the requests served within a certain time (ms)
  50%     14
  66%     20
  75%     27
  80%     31
  90%     50
  95%    220
  98%    395
  99%    464
 100%    584 (longest request)

JMeter

  相对于AB来说,JMeter更加强大。Apache JMeter是Apache组织开发的基于Java的压力测试工具。JMeter 可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。

参考文章:JMeter使用入门

Windows 7 安装
  1. 进入官网下载
  2. 将下载后的apache-jmeter-4.0.zip解压
  3. 进入解压目录中的bin目录,运行jmeter.bat
运行演示
创建线程组
图 创建线程租
图 线程组参数配置

Number of Threads(users) : 线程数、虚拟用户数

Ramp-Up Period(in second) : 虚拟用户增长时长。理解:假设现在有一个考勤系统 ,所有的用户都不是同时登陆的,实际使用场景是在某段时间内,用户会陆陆续续的进行考勤,而这个参数大概理解就是这个意思,考勤是从8点40分到9点10分,那么这个参数就是30分钟*60秒,意味着指定用户请求在规定时间内完成请求。

Loop Count : 循环次数,每个虚拟用户循环的次数,如果勾选Forever则会一直进行下去,默认是1

添加请求
图 添加请求
图 请求参数配置

为请求添加结果监听:图形结果(Graph Results)与查看结果树(View Results Tree)

图 添加监听结果
结果分析
图 总体结果分析
图 具体结果分析

代码模拟

CountDownLatch

  CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

图 CountDownLatch运行流程图

假设计数器的值为3,线程A调用await()方法之后,当前线程就进入了等待状态, 之后在其他线程中执行countDown(),计数器就会 - 1 ,该操作线程继续执行,当计数器从3变成0之后,线程A继续执行。

CountDownLatch这个类可以阻塞线程,保证线程在某种特定的条件下,继续执行。

Semaphore
图 Semaphore概念流程

  Semaphore翻译成字面意思为 信号量,Semaphore可以阻塞进程并且控制同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

  CountDownLatch与Semaphore在使用时,通常会与线程池配合使用

  Semaphore适合控制并发数,CountDownLatch比较适合保证线程执行完后再执行其他处理,因此模拟并发时,使用两者结合起来是最好的。

并发模拟代码实现
@Slf4j
@NotThreadSafe //线程不安全的
public class ConcurrencyTest {
    // 请求总数
    public static int clientTotal = 5000;
    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static int count = 0;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定义计数器闭锁
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire(); //获取信号量,否则会阻塞
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown(); //每执行一次则减1
            });
        }
        countDownLatch.await();
        executorService.shutdown(); //关闭线程池
        log.info("count:{}", count);
    }
    //线程不安全
    private static void add() {
        count++;
    }
}

推荐阅读更多精彩内容