您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
测试聊并发-入门篇
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
测试聊并发-入门篇
jd****
2024-10-15
IP归属:北京
100浏览
## 背景 在现代软件测试的广阔领域中,我们的工作不仅限于确保功能符合产品和业务需求的严格标准。随着用户对应用性能的期望水涨船高,性能测试已成为衡量软件质量的关键指标。特别是在服务端接口的性能测试中,我们面临的挑战不仅仅是处理单个请求的效率,更在于如何在多用户同时访问时保持系统的稳定性和响应速度。并发编程和测试,作为性能测试的核心,对于评估系统在高负载情况下的表现、识别潜在的性能瓶颈、以及优化资源配置具有至关重要的作用。 并发编程是一门艺术,它要求开发者在多线程或多进程的环境中精心编排代码,以实现资源的高效共享和任务的并行执行。这不仅需要深厚的编程功底,更需要对并发模型、同步机制和线程安全性有深刻的认识。而在测试领域,性能测试工程师必须精通如何构建并发测试场景,运用工具模拟真实的高并发环境,以及如何从测试结果中提炼出有价值的洞察,以指导性能的持续优化。 本文将深入剖析并发编程的深层原理、面临的挑战以及采纳的最佳实践。同时,我们将探讨并发测试的策略、工具和技术,并通过实际案例的分析,阐释如何在软件开发生命周期中有效地整合并发测试,以及如何利用并发测试来显著提升系统的性能和可靠性。 ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-09-12-22-00FvJKyCZ7WS08Nzd.jpg) ### 多线程基础和作用 #### 进程与线程的区别 ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-09-18-18-38GsDBl6KrOdxK6sm.jpg) 资源分配:进程是资源分配的基本单位,线程是CPU调度和执行的基本单位。 独立性:进程是独立运行的,而线程则依赖于进程。 内存分配:进程有自己的内存空间,线程共享进程的内存空间。 开销:线程的创建和切换开销小于进程。 并发性:线程可以提高程序的并发性,因为它们可以并行执行。 #### Java中线程的创建方式 方式一.继承Thread类 当你创建一个继承自Thread类的子类时,你需要重写run方法,该方法包含了线程要执行的代码。然后,你可以通过创建这个子类的实例并调用其start方法来启动线程。 ``` class MyThread extends Thread { @Override public void run() { // 线程要执行的代码 System.out.println("线程运行中..."); } } public class ThreadExample { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); // 启动线程 } } ``` 方式二.实现Runnable接口 另一种创建线程的方式是实现Runnable接口。你需要创建一个实现了Runnable接口的类,然后创建该类的实例,并把这个实例传递给Thread类的构造函数。最后,通过调用Thread对象的start方法来启动线程。 ``` class MyRunnable implements Runnable { @Override public void run() { // 线程要执行的代码 System.out.println("线程运行中..."); } } public class RunnableExample { public static void main(String[] args) { MyRunnable r = new MyRunnable(); Thread t = new Thread(r); t.start(); // 启动线程 } } ``` 比较两种方式 灵活性:实现Runnable接口比继承Thread类更灵活,因为Java不支持多重继承,但可以实现多个接口。 资源管理:如果你需要多个线程共享同一个资源,实现Runnable接口是更好的选择,因为你可以定义一个资源类,然后创建多个Runnable实例来共享这个资源。 代码重用:实现Runnable接口允许你将线程的运行代码与线程的控制代码分离,这有助于代码重用。 在实际开发中,推荐使用实现Runnable接口的方式来创建线程,因为它提供了更好的灵活性和代码重用性,但也需考虑实际情况选择使用。 ### 线程生命周期 ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-09-19-19-07omm7TF19lJC8RhWu.jpg) - 新建(New)、可运行(Runnable)、阻塞(Blocked)、正在运行(Running)、终止(Terminated)等状态的解释。 新建(New): 线程对象已经被创建,但还没有调用start()方法。在这个状态下,线程还没有开始执行。 可运行(Runnable): 线程已经调用了start()方法,此时线程处于可运行状态。可运行状态包括了操作系统线程的就绪(Ready)和运行(Running)状态。线程可能正在运行,也可能正在等待CPU时间片,因为可运行状态的线程会与其他线程共享CPU资源。 阻塞(Blocked): 线程因为等待一个监视器锁(比如进入一个同步块)而无法继续执行的状态。在这种情况下,线程会一直等待直到获取到锁。阻塞状态通常发生在多个线程尝试进入一个同步方法或同步块时,但只有一个线程能够获得锁。 正在运行(Running): 线程正在执行其run()方法的代码。这个状态是可运行状态的一个子集,表示线程当前正在CPU上执行。 注意:在Java官方文档中,并没有明确区分“可运行”和“正在运行”这两个状态,通常将它们统称为“可运行(Runnable)”状态。 终止(Terminated): 线程的运行结束。这可能是因为线程正常执行完任务,或者因为某个未捕获的异常导致线程结束。一旦线程进入终止状态,它就不能再被启动或恢复。 ### 线程同步 同步指的是在多线程环境中,控制多个线程对共享资源的访问顺序,以防止数据不一致和竞态条件。同步机制确保了当一个线程访问某个资源时,其他线程不能同时访问该资源。 数据一致性:防止多个线程同时修改同一数据,导致数据不一致。 线程安全:确保程序在多线程环境下能够正确运行,不会因为线程的并行执行而出现错误。 性能优化:合理的同步可以提高程序的并发性能,避免不必要的线程阻塞和上下文切换。 synchronized关键字的使用 synchronized 是 Java 中用于同步的一个关键字,它可以用于方法或代码块,确保同一时间只有一个线程可以执行该段代码。 ``` 同步方法 public synchronized void myMethod() { // 需要同步的代码 } ``` ``` 同步代码块 public void myMethod() { synchronized(this) { // 需要同步的代码 } } ``` Locks&ReentrantLock Java 提供了更灵活的锁机制,称为 Locks,其中最常用的是 ReentrantLock。 Locks:提供了比 synchronized 更灵活的锁定机制,如尝试锁定、定时锁定、可中断的锁定等。 ReentrantLock:是一种可重入的互斥锁,支持完全的锁定操作,可以被同一个线程多次获得,但必须释放相同次数。 使用 ReentrantLock 的基本步骤: 创建 ReentrantLock 对象。 在需要同步的代码块前后调用 lock() 和 unlock() 方法。 确保在 finally 块中释放锁,以避免死锁 ``` import java.util.concurrent.locks.ReentrantLock; public class Example { private final ReentrantLock lock = new ReentrantLock(); public void myMethod() { lock.lock(); try { // 需要同步的代码 } finally { lock.unlock(); } } } ``` 线程同步是确保多线程程序正确性和性能的关键技术。synchronized 和 ReentrantLock 提供了不同的同步机制,开发者可以根据具体需求选择合适的同步方式。正确使用同步机制可以避免数据不一致和竞态条件,提高程序的稳定性和性能。 ### 线程间通信 线程间通信是多线程编程中的一个重要概念,它允许线程之间进行数据交换和状态同步。在 Java 中,线程间通信主要通过等待/通知机制和条件变量来实现。 #### 等待/通知机制(wait()、notify()、notifyAll()) wait():当一个线程调用 wait() 方法时,它会释放对象的锁,并进入该对象的等待池(wait set)中等待。其他线程可以调用 notify() 或 notifyAll() 方法来唤醒等待池中的线程。 notify():唤醒在该对象上等待的单个线程。选择哪个线程是不确定的。 notifyAll():唤醒在该对象上等待的所有线程。 ``` public class Message { private String content; private boolean empty = true; public synchronized String take() throws InterruptedException { while (empty) { wait(); } empty = true; notifyAll(); return content; } public synchronized void put(String content) throws InterruptedException { while (!empty) { wait(); } empty = false; this.content = content; notifyAll(); } } ``` #### 条件变量(Condition) 条件变量提供了一种更灵活的线程间通信方式。Condition 接口是 java.util.concurrent.locks 包的一部分,它与 Lock 接口一起使用。 await():类似于 wait(),但需要在 Condition 对象上调用。 signal():类似于 notify(),但需要在 Condition 对象上调用。 signalAll():类似于 notifyAll(),但需要在 Condition 对象上调用。 ``` import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class Message { private String content; private boolean empty = true; private final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); public void put(String content) throws InterruptedException { lock.lock(); try { while (!empty) { notEmpty.await(); } empty = false; this.content = content; notEmpty.signal(); } finally { lock.unlock(); } } public String take() throws InterruptedException { lock.lock(); try { while (empty) { notEmpty.await(); } empty = true; String result = content; notEmpty.signal(); return result; } finally { lock.unlock(); } } } ``` ### 线程池 ![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-09-24-18-26NS26bHhHmMN9MqMr.jpg) 线程池是一种执行器(Executor),用于在一个后台线程中执行任务。线程池的主要目的是减少在创建和销毁线程时所产生的性能开销。通过重用已经创建的线程来执行新的任务,线程池提高了程序的响应速度,并且提供了更好的系统资源管理。 #### Executor框架的使用 Java的java.util.concurrent包提供了Executor框架,它是一个用于管理线程的框架,包括线程池的管理。Executor框架的核心接口是Executor和ExecutorService。 Executor:一个执行提交的Runnable任务的接口。 ExecutorService:Executor的子接口,提供了管理任务生命周期的方法,如关闭线程池、提交异步任务等。 #### 如何创建和使用不同类型的线程池 ``` import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExample { public static void main(String[] args) { // 创建一个固定大小的线程池 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4); // 创建一个缓存线程池 ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 创建一个单线程池 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); // 提交任务给线程池 for (int i = 0; i < 10; i++) { final int index = i; fixedThreadPool.submit(() -> { System.out.println("执行任务:" + index + " 线程:" + Thread.currentThread().getName()); }); } // 关闭线程池 fixedThreadPool.shutdown(); cachedThreadPool.shutdown(); singleThreadExecutor.shutdown(); } } ``` ### 并发集合 #### 传统的集合类在多线程环境下的问题 传统的集合类(如 ArrayList、LinkedList、HashMap 等)并不是线程安全的。这意味着,如果在多线程环境下,多个线程同时对这些集合进行读写操作,可能会导致以下几种问题 数据不一致:当多个线程同时修改集合时,可能会导致集合的状态不一致。例如,一个线程正在遍历列表,而另一个线程正在添加或删除元素,这可能导致遍历过程中出现 ConcurrentModificationException。 竞态条件:当多个线程并发访问集合并且至少有一个线程在修改集合时,就会发生竞态条件。这意味着最终结果依赖于线程执行的顺序,这可能导致不可预测的结果。 脏读:一个线程可能读取到另一个线程修改了一半的数据,这种读取被称为“脏读”。 幻读:在一个事务中,多次查询数据库,由于其他事务插入了行,导致原本满足条件的查询结果集中出现了“幻影”行。 不可重复读:在一个事务内,多次读取同一数据集合,由于其他线程的修改,导致每次都得到不同的数据,这被称为不可重复读。 #### 通过以下几种策略解决多线程环境问题 使用同步包装器:Java提供了一些同步包装器,如 Collections.synchronizedList、Collections.synchronizedMap 等,可以将非线程安全的集合包装成线程安全的。 使用并发集合:Java的 java.util.concurrent 包提供了一些线程安全的集合类,如 ConcurrentHashMap、CopyOnWriteArrayList 等,它们内部实现了必要的同步机制。 使用锁:可以使用 synchronized 关键字或 ReentrantLock 对集合的操作进行显式同步。 使用原子类:对于基本数据类型的集合,可以使用 java.util.concurrent.atomic 包中的原子类,如 AtomicInteger、AtomicReference 等。 使用不可变集合:不可变集合一旦创建就不能被修改,因此是线程安全的。可以使用 Collections.unmodifiableList、Collections.unmodifiableMap 等方法创建不可变集合。 使用线程局部变量:如果每个线程都需要有自己的集合副本,可以使用 ThreadLocal 类。 避免共享:如果可能,避免在多个线程间共享集合,每个线程使用独立的集合可以避免同步问题。 ### 并发设计模式 #### 生产者-消费者模式(Producer-Consumer Pattern) 生产者-消费者模式是一种常见的并发设计模式,用于协调生产者线程和消费者线程之间的工作。生产者线程负责生成数据,消费者线程负责处理数据。它们之间通常通过一个共享的缓冲区(如队列)进行通信。这个模式可以有效地解耦生产者和消费者的工作,提高程序的并发性能。 ``` BlockingQueue<Work> queue = new LinkedBlockingQueue<>(); class Producer extends Thread { public void run() { while (true) { Work item = produce(); queue.put(item); } } Work produce() { // 生产数据 return new Work(); } } class Consumer extends Thread { public void run() { while (true) { Work item = queue.take(); consume(item); } } void consume(Work item) { // 消费数据 } } ``` #### 读写锁模式(Reader-Writer Lock Pattern) 读写锁模式允许多个线程同时读取共享资源,但写入操作是互斥的。这种模式适用于读多写少的场景,可以提高程序的并发性能。 ``` class ReadWriteResource { private final ReadWriteLock lock = new ReentrantReadWriteLock(); public void read() { lock.readLock().lock(); try { // 执行读取操作 } finally { lock.readLock().unlock(); } } public void write() { lock.writeLock().lock(); try { // 执行写入操作 } finally { lock.writeLock().unlock(); } } } ``` #### 线程池模式(ThreadPool Pattern) 线程池模式通过复用一组线程来执行多个任务,减少了线程创建和销毁的开销。线程池可以控制并发线程的数量,提高资源利用率。 ``` ExecutorService executor = Executors.newFixedThreadPool(10); executor.submit(() -> { // 执行任务 }); executor.shutdown(); ``` ### 案例分析 写了几个多线程并发的小demo,有需要可以联系获取仓库权限 注:文章有很多瑕疵,欢迎各位大佬批评指正
上一篇:通过Forcebot压测实践简述“并发模式”与“RPS模式”两种模式的区别
下一篇:90后程序员的职业成长漫谈
jd****
文章数
6
阅读量
73
作者其他文章
01
HarmonyOS-安装篇(DevEco Studio)
一、下载可通过官网下载: https://developer.harmonyos.com/cn/develop/devevo-studio二、安装(经过实践了)三、配置nodejs&ohpm配置SDK配置等待(这块还是稍微有点慢,不要焦急)四、应用(demo)HelloWorld工程创建(选择应用模板,继续点击Next即可创建完成)添加设备(此处我添加的是手机虚拟设备)设备命名(我是使用的默认的
01
探索Playwright:前端自动化测试的新纪元
背景在前端开发中,自动化测试是确保软件质量和用户体验的关键环节。随着Web应用的复杂性不断增加,手动测试已经无法满足快速迭代和持续交付的需求。自动化测试通过模拟用户与应用的交互,能够高效地执行重复性测试任务,加快测试周期,提升测试覆盖率,从而更早地发现缺陷和问题。这不仅提高了软件的稳定性和可靠性,还降低了维护成本,并为创新和功能增强提供了更多的时间和资源。在众多自动化测试工具中,Playwrigh
01
测试聊并发-入门篇
背景在现代软件测试的广阔领域中,我们的工作不仅限于确保功能符合产品和业务需求的严格标准。随着用户对应用性能的期望水涨船高,性能测试已成为衡量软件质量的关键指标。特别是在服务端接口的性能测试中,我们面临的挑战不仅仅是处理单个请求的效率,更在于如何在多用户同时访问时保持系统的稳定性和响应速度。并发编程和测试,作为性能测试的核心,对于评估系统在高负载情况下的表现、识别潜在的性能瓶颈、以及优化资源配置具有
01
Autobots应用探索:实践中的思考与发现
背景背景1:作为一名测试,日常工作中必不可少的几个环节是查看需求文档、编写测试用例、处理线上问题、能力提升等,基于集团的https://xxx.jd.com/工具能一次性帮我们把这些事情都做了;背景2:作为XXX共建项目的成员之一同时也是第一批用户,我用它做了几个测试实践,和大佬们一起探讨交流。实践一,快速搜索需求文档模糊搜索这个场景多数用于我们不清楚自己到底要找的是哪个需求文档了,可能只是大概对
jd****
文章数
6
阅读量
73
作者其他文章
01
HarmonyOS-安装篇(DevEco Studio)
01
探索Playwright:前端自动化测试的新纪元
01
Autobots应用探索:实践中的思考与发现
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号