0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

测试聊并发-入门篇

京东云 来源:京东保险 张新磊 作者:京东保险 张新磊 2024-10-15 15:23 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

作者:京东保险 张新磊

背景

在现代软件测试的广阔领域中,我们的工作不仅限于确保功能符合产品和业务需求的严格标准。随着用户对应用性能的期望水涨船高,性能测试已成为衡量软件质量的关键指标。特别是在服务端接口的性能测试中,我们面临的挑战不仅仅是处理单个请求的效率,更在于如何在多用户同时访问时保持系统的稳定性和响应速度。并发编程和测试,作为性能测试的核心,对于评估系统在高负载情况下的表现、识别潜在的性能瓶颈、以及优化资源配置具有至关重要的作用。

并发编程是一门艺术,它要求开发者在多线程或多进程的环境中精心编排代码,以实现资源的高效共享和任务的并行执行。这不仅需要深厚的编程功底,更需要对并发模型、同步机制和线程安全性有深刻的认识。而在测试领域,性能测试工程师必须精通如何构建并发测试场景,运用工具模拟真实的高并发环境,以及如何从测试结果中提炼出有价值的洞察,以指导性能的持续优化。

本文将深入剖析并发编程的深层原理、面临的挑战以及采纳的最佳实践。同时,我们将探讨并发测试的策略、工具和技术,并通过实际案例的分析,阐释如何在软件开发生命周期中有效地整合并发测试,以及如何利用并发测试来显著提升系统的性能和可靠性。

wKgaomcOGIaAYRRFAA8I_5ZNG3k231.jpg

多线程基础和作用

进程与线程的区别

wKgZomcOGIeAZcRUAAW-odY7m30058.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接口的方式来创建线程,因为它提供了更好的灵活性和代码重用性,但也需考虑实际情况选择使用。

线程生命周期

wKgaomcOGImAVYsTAAWkaVSPzxw031.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();
        }
    }
}

线程池


线程池是一种执行器(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 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,有需要可以联系获取仓库权限

注:文章有很多瑕疵,欢迎各位大佬批评指正

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 测试
    +关注

    关注

    8

    文章

    6027

    浏览量

    130699
  • 代码
    +关注

    关注

    30

    文章

    4941

    浏览量

    73127
收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    AppGallery Connect(HarmonyOS 5及以上) --公开测试创建并发测试版本(一)

    )的公开测试版本会自动下架。 发布测试版本 首先,您需创建并发测试版本。最多支持1个公开测试版本在架, 1.登录AppGallery Co
    发表于 09-26 17:24

    创建并发测试版本(一)

    创建并发测试版本,并选择您要分发的测试群组。邀请测试最多允许100个版本同时在架,邀请测试和公开测试
    发表于 09-16 15:21

    【离线语音】安信可VC-01/02教程:中级入门篇

    教程 【快速上手】 安信可离线语音模组 VC-01、VC-02 系列教程 【中级入门篇】 安信可离线语音模组 VC-01、VC-02 系列教程 【高级进阶】 安信可离线语音模组 VC-01、VC-02
    的头像 发表于 07-31 09:33 624次阅读
    【离线语音】安信可VC-01/02教程:中级<b class='flag-5'>入门篇</b>

    ESP32-运行网页服务器(Web Server)-实用

    在前一文章《ESP32-运行网页服务器(WebServer)-入门篇》,我们介绍了ESP32运行网页服务器(WebServer)的原理,然后我们基于ESP32实现了一个demo代码;看到很多同学都留言发表了自己的看法,有很多同学都基于这个原理实现了很多有意思的应用;这里
    的头像 发表于 07-28 18:05 2798次阅读
    ESP32-运行网页服务器(Web Server)-实用<b class='flag-5'>篇</b>

    鸿蒙5开发宝藏案例分享---应用并发设计

    一句 : 鸿蒙的并发模型是为分布式而生的设计,吃透这些案例后你会发现: 折叠屏/多端适配不再头疼 性能调优有迹可循 复杂业务逻辑清晰解耦 遇到坑点欢迎回讨论~ 觉得有用记得点赞收藏?
    发表于 06-12 16:19

    每周推荐!电子工程师自学资料及各种电路解析

    —— 提高 本文共3册,由于资料内存过大,分开上传,有需要的朋友可以去主页搜索下载哦~电子工程师自学速成分为:入门篇、提高和设计,本文为提高
    发表于 05-19 18:20

    电子工程师自学速成 —— 提高

    本文共3册,由于资料内存过大,分开上传,有需要的朋友可以去主页搜索下载哦~ 电子工程师自学速成分为:入门篇、提高和设计,本文为提高;内容包括模拟电路和数字电路两大部分,其中模拟
    发表于 05-15 15:56

    电子工程师自学速成——入门篇

    本文共3册,由于资料内存过大,分开上传,有需要的朋友可以去主页搜索下载哦~ 电子工程师自学速成分为:入门篇、提高和设计,本文为入门篇,内容包括电子技术
    发表于 05-15 15:50

    【「零基础开发AI Agent」阅读体验】+ 入门篇学习

    的是基础,主要从为什么要学习AI Agent和开发AI Agent的知识储备入手进行介绍。作为入门AI Agent的小白还是很有必要学习的。这里将一些重要观点作个归纳 1.AI Agent=大模型+记忆
    发表于 05-02 09:26

    【「零基础开发AI Agent」阅读体验】总体预览及入门篇

    基础知识有所补充,另外书本后面的案例也会对Ai的应用产生一些启发. 首先老规矩,先看一下目录结构 包含3大主题: 入门篇:介绍了Agent的概念、发展、与Prompt和Copilot的区别
    发表于 04-20 21:53

    典型电路原理、电路识图从入门到精通等资料

    1、电路识图从入门到精通高清电子资料 由浅入深地介绍了电路图的基础知识、典型单元电路的识图方法,通过“入门篇”和“精通篇”循序渐进、由浅入深地介绍了电路图的基础知识、典型单元电路的识图方法,以及典型
    的头像 发表于 04-15 15:53 1.8w次阅读
    典型电路原理、电路识图从<b class='flag-5'>入门</b>到精通等资料

    新概念51单片机C语言教程入门、提高、开发、拓展全攻略

    资料介绍 从实际应用入手,以实验过程和实验现象为主导,循序渐进地讲述51单片机C语言编程方法以及51单片机的硬件结构和功能应用。全书共分5,分别为入门篇、内外部资源操作、提高、实
    发表于 04-15 13:57

    电路识图从入门到精通高清电子资料

    由浅入深地介绍了电路图的基础知识、典型单元电路的识图方法,通过“入门篇”和“精通篇”循序渐进、由浅入深地介绍了电路图的基础知识、典型单元电路的识图方法,以及典型小家电、电动车、洗衣机、电冰箱、空调器
    发表于 04-10 16:22

    RK3568驱动指南|第三-并发与竞争-第19章 并发与竞争实验

    RK3568驱动指南|第三-并发与竞争-第19章 并发与竞争实验
    的头像 发表于 02-24 16:26 845次阅读
    RK3568驱动指南|第三<b class='flag-5'>篇</b>-<b class='flag-5'>并发</b>与竞争-第19章 <b class='flag-5'>并发</b>与竞争实验

    超详细“零”基础kafka入门篇

    1、认识kafka 1.1 kafka简介 Kafka 是一个分布式流媒体平台 kafka官网:http://kafka.apache.org/ (1)流媒体平台有三个关键功能: 发布和订阅记录流 ,类似于消息队列或企业消息传递系统。 以 容错的持久方式存储记录流 。 记录发生时处理流。 (2)Kafka通常用于两大类应用: 构建可在 系统或应用程序之间 可靠获取数据的实时流数据管道 构建转换或响应数据流的实时流应用程序 要了解Kafka如何做这些事情,让我们深入探讨Kafka的能力。 (3)首先是几个概念:
    的头像 发表于 12-18 09:50 4766次阅读
    超详细“零”基础kafka<b class='flag-5'>入门篇</b>