ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java并发测试实战:从单元测试到集成测试的四种方法

Java并发测试实战:从单元测试到集成测试的四种方法 1. 项目概述为什么并发测试是每个开发者的必修课最近在排查一个线上服务偶发性响应变慢的问题日志里风平浪静单接口压测也一切正常但只要用户量一上来系统就开始“咳嗽”。最后定位到问题是一个看似不起眼的静态变量在多线程环境下被意外修改导致了数据错乱。这个经历让我再次深刻体会到并发测试绝不是性能压测的附属品而是保障现代多线程、分布式系统稳定性的生命线。无论你是开发一个高并发的电商秒杀系统还是一个简单的后台管理工具只要你的代码运行在多核CPU上、部署在多个容器里并发问题就可能像幽灵一样潜伏着只在特定条件下给你致命一击。所谓并发测试核心目标就是模拟多个用户或线程在同一时间段内对系统资源如数据库行、内存对象、API接口进行竞争性操作以发现那些在单线程环境下永远无法暴露的缺陷数据竞争、死锁、活锁、资源耗尽、状态不一致等等。很多人觉得并发测试高深莫测是测试工程师的专属领域其实不然。掌握几种简单、实用的并发测试方法应该是每一位一线开发者的基本功。它能帮助你在代码上线前就提前“预演”高并发场景把问题扼杀在摇篮里。接下来我就结合自己多年的踩坑经验分享几种在开发阶段就能快速上手的并发测试方法从理论到实操让你也能为自己的代码加上一道可靠的“并发安全锁”。2. 并发测试的核心思路与常见误区在动手写测试代码之前我们必须先理清思路避开几个常见的认知误区。并发测试不是“用Jmeter猛灌流量”那么简单它有着更精细的划分和目标。2.1 并发测试的三大核心目标首先我们要明确测试想发现什么线程安全缺陷这是最常见的。当多个线程不加控制地访问和修改同一块共享数据如一个全局List、一个静态Map、一个单例对象内的属性时就会导致数据状态不可预测。例如经典的“i”问题在并发下会导致计数不准。资源同步问题包括死锁两个以上的线程互相等待对方释放锁、活锁线程不断改变状态以试图获取资源但始终无法取得进展和资源饥饿某些线程长期无法获得所需资源。这类问题通常与锁synchronized、ReentrantLock的使用不当有关。系统资源与性能边界在高并发下系统资源如数据库连接池、线程池、内存、CPU是否会快速耗尽响应时间是否线性增长吞吐量是否达到瓶颈这关系到系统的可伸缩性。2.2 必须避开的两个典型误区基于以上目标我们在设计测试时需要警惕误区一把压力测试等同于并发测试。这是最大的误解。压力测试Stress Test主要关注系统在极限负载下的表现和恢复能力比如用每秒数万的请求把接口打满。而并发测试Concurrency Test更关注正确性负载可能不大但操作序列设计得非常“刁钻”旨在触发特定的竞态条件。一个接口能承受1万QPS不代表它在10个用户同时修改同一条数据时不会出错。误区二认为“跑一遍没出错就是线程安全”。并发缺陷具有极强的偶发性。由于线程调度由操作系统决定具有随机性一个存在数据竞争的程序可能运行成千上万次都是正确的结果只在某种特定的、极难重现的线程交错顺序下才会出错。因此并发测试需要反复、大量地执行并尽可能增加线程调度的不确定性以提高发现问题的概率。理解了这些我们就可以选择合适的方法和工具了。下面介绍的几种方法其有效性正是基于它们能够系统地放大并发冲突的可能性。3. 方法一基于JUnit与ExecutorService的轻量级单元并发测试这是开发阶段最快捷、成本最低的测试方式适合对某个具体的类或方法进行并发安全性验证。我们不需要引入复杂的测试框架利用Java标准库的ExecutorService线程池就能搭建一个简单的测试环境。3.1 测试场景搭建假设我们有一个简单的计数器服务UnsafeCounter它存在明显的线程安全问题public class UnsafeCounter { private int count 0; // 非线程安全的递增方法 public void increment() { count; // 这是一个“读取-修改-写入”的非原子操作 } public int getCount() { return count; } }我们要测试increment方法在并发下的行为。以下是基于JUnit 5的测试类import org.junit.jupiter.api.Test; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.*; class UnsafeCounterTest { Test void testIncrementConcurrently() throws InterruptedException { // 1. 定义并发线程数和工作任务总数 int threadCount 10; int totalIncrements 10000; UnsafeCounter counter new UnsafeCounter(); // 2. 创建固定大小的线程池 ExecutorService executorService Executors.newFixedThreadPool(threadCount); // 使用CountDownLatch协调所有线程同时开始 CountDownLatch startLatch new CountDownLatch(1); // 使用CountDownLatch等待所有线程结束 CountDownLatch endLatch new CountDownLatch(totalIncrements); // 3. 提交任务 for (int i 0; i totalIncrements; i) { executorService.submit(() - { try { startLatch.await(); // 所有线程在此等待 counter.increment(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { endLatch.countDown(); // 每个任务完成时计数减一 } }); } // 4. 释放“起跑线”所有线程开始并发执行 startLatch.countDown(); // 5. 等待所有任务完成 endLatch.await(); // 6. 关闭线程池 executorService.shutdown(); // 7. 验证结果 System.out.println(Expected count: totalIncrements); System.out.println(Actual count: counter.getCount()); // 因为存在线程安全问题实际结果几乎必然小于预期 assertNotEquals(totalIncrements, counter.getCount()); } }3.2 关键技巧与避坑指南这个简单的测试里藏着几个确保测试有效的关键点1. 使用CountDownLatch制造“并发齐射”startLatch.await()让所有提交的任务线程都准备就绪并阻塞在同一个点上。当主线程调用startLatch.countDown()时所有任务线程近乎同时被释放极大地增加了它们对count这个临界资源竞争的激烈程度和随机性。如果不这样做线程可能按提交顺序依次启动降低了并发冲突的概率。2. 任务数要远大于线程数例子中我们用了10个线程执行10000次递增。让每个线程执行多次操作可以增加线程上下文切换的次数从而创造出更多种可能的线程执行序列更容易触发那些隐藏很深的竞态条件。3. 断言“不相等”而非“相等”对于已知非线程安全的代码我们的测试目的是验证它在并发下会出错。因此断言assertNotEquals是合理的。但对于一个你希望它是线程安全的代码你需要在多次运行中断言其结果始终等于预期值。可以考虑在循环中多次运行整个测试逻辑。4. 别忘了关闭线程池在测试方法中创建了线程池一定要在最后调用shutdown()。更好的做法是使用try-with-resources如果ExecutorService实现了AutoCloseable或在AfterEach方法中清理防止线程泄漏影响其他测试。注意这种测试方法在常规的IDE如IntelliJ IDEA中直接运行Test方法可能无法充分暴露问题因为测试运行器的环境可能比较“温和”。建议使用Maven或Gradle在命令行中反复执行测试如mvn test -DtestUnsafeCounterTest或者将测试方法放在一个循环中执行多次。4. 方法二利用CompletableFuture进行异步流程的并发模拟如果你的业务逻辑涉及多个异步任务的组合、编排那么CompletableFuture是进行并发测试的绝佳工具。它不仅能模拟并发执行还能方便地测试各种任务依赖关系下的并发行为。4.1 测试异步编排下的资源竞争考虑一个常见的场景用户下单后需要同时调用库存服务、优惠券服务和风控服务三者都成功后再执行扣款。我们如何测试这三个异步调用对共享资源比如更新同一个订单状态的并发操作import org.junit.jupiter.api.Test; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.*; class AsyncOrderServiceTest { Test void testConcurrentStatusUpdate() throws Exception { // 模拟一个共享的订单状态管理器非线程安全 class OrderStatusManager { private String status INIT; public void updateStatus(String newStatus) { // 模拟一个非原子性的复杂状态转换 String old this.status; // 此处模拟一个耗时操作放大竞态窗口 try { Thread.sleep(ThreadLocalRandom.current().nextInt(5)); } catch (InterruptedException e) {} this.status newStatus; System.out.println(Thread.currentThread().getName() : Updated status from \ old \ to \ newStatus \); } public String getStatus() { return status; } } OrderStatusManager manager new OrderStatusManager(); AtomicInteger updateCount new AtomicInteger(0); // 使用CompletableFuture模拟三个异步服务调用 CompletableFutureVoid future1 CompletableFuture.runAsync(() - { manager.updateStatus(CHECKING_INVENTORY); updateCount.incrementAndGet(); }); CompletableFutureVoid future2 CompletableFuture.runAsync(() - { manager.updateStatus(CHECKING_COUPON); updateCount.incrementAndGet(); }); CompletableFutureVoid future3 CompletableFuture.runAsync(() - { manager.updateStatus(RISK_CONTROLLING); updateCount.incrementAndGet(); }); // 等待所有异步任务完成 CompletableFuture.allOf(future1, future2, future3).join(); // 验证三个任务都执行了更新 assertEquals(3, updateCount.get()); // 但最终状态可能不是我们预期的最后一个而是由线程竞争决定的任意一个 System.out.println(Final status: manager.getStatus()); // 断言可能失败证明存在并发问题 // assertTrue(Set.of(CHECKING_INVENTORY, CHECKING_COUPON, RISK_CONTROLLING).contains(manager.getStatus())); } }4.2 使用thenCombine测试合并操作的并发安全CompletableFuture的thenCombine方法常用于合并两个异步任务的结果。这是测试合并逻辑线程安全的经典场景。Test void testThenCombineConcurrency() throws Exception { // 模拟一个非线程安全的计算结果收集器 class ResultAggregator { private ListInteger results new ArrayList(); public void addResult(int result) { // ArrayList的add方法非线程安全 results.add(result); } public int getSum() { return results.stream().mapToInt(Integer::intValue).sum(); } } ResultAggregator aggregator new ResultAggregator(); // 创建两个异步计算任务 CompletableFutureInteger futureA CompletableFuture.supplyAsync(() - { try { Thread.sleep(50); } catch (InterruptedException e) {} return 10; }); CompletableFutureInteger futureB CompletableFuture.supplyAsync(() - { try { Thread.sleep(30); } catch (InterruptedException e) {} return 20; }); // 合并结果并在合并后执行添加操作这里存在并发风险 CompletableFutureVoid combinedFuture futureA.thenCombine(futureB, (a, b) - { // 这个BiFunction在哪个线程执行可能是futureA或futureB的线程也可能是调用thenCombine的线程。 // 如果在此直接操作共享的aggregator需要小心。 int sum a b; aggregator.addResult(sum); // 潜在并发点 return null; }); combinedFuture.join(); // 等待合并完成 // 由于ArrayList的并发问题这里可能会抛出ArrayIndexOutOfBoundsException // 或者getSum()的结果不正确如果add操作丢失了数据 System.out.println(Aggregator sum: aggregator.getSum()); }实操心得CompletableFuture的回调函数如thenApply、thenAccept、thenCombine中的函数执行线程是不确定的。它可能在完成当前阶段的线程上执行也可能在调用回调的线程如join()或get()的线程上执行。如果多个回调函数并行执行并访问共享资源必须像对待普通多线程代码一样考虑同步。一个最佳实践是在回调函数内部只处理传入的参数和局部变量将需要线程安全操作的部分封装到专门的同步类或使用并发集合中。5. 方法三使用ThreadLocalRandom与循环屏障制造不确定性并发缺陷的复现依赖于“巧合”的线程执行时序。我们可以主动在代码中插入一些可控的随机性和同步点来主动制造这种“巧合”提高测试的强度。ThreadLocalRandom和CyclicBarrier是两位好帮手。5.1 利用随机休眠放大竞态窗口大多数竞态条件发生在两个线程操作的间隙非常小的时候。我们可以通过在线程的关键操作前后随机休眠一小段时间来人为地放大这个“竞态窗口”使得线程交错更容易发生。Test void testWithRandomDelay() throws InterruptedException { class BankAccount { private double balance 100.0; public void withdraw(double amount) { // 1. 读取余额 double current this.balance; // 插入随机延迟让其他线程有机会在此刻介入 try { Thread.sleep(ThreadLocalRandom.current().nextInt(0, 3)); } catch (InterruptedException e) {} // 2. 检查并更新余额 if (current amount) { // 再次插入随机延迟 try { Thread.sleep(ThreadLocalRandom.current().nextInt(0, 3)); } catch (InterruptedException e) {} this.balance current - amount; System.out.println(Thread.currentThread().getName() 取款 amount 成功余额: this.balance); } else { System.out.println(Thread.currentThread().getName() 取款 amount 失败余额不足); } } public double getBalance() { return balance; } } BankAccount account new BankAccount(); int threadCount 5; ExecutorService executor Executors.newFixedThreadPool(threadCount); CountDownLatch latch new CountDownLatch(threadCount); // 模拟5个线程同时取款80元 for (int i 0; i threadCount; i) { executor.submit(() - { try { account.withdraw(80.0); } finally { latch.countDown(); } }); } latch.await(); executor.shutdown(); double finalBalance account.getBalance(); System.out.println(最终余额: finalBalance); // 由于存在竞态条件最终余额可能是错误的例如20, 60, 甚至-60而不是正确的-300100-5*80 // 理论上最多只有一个线程能成功取款最终余额应为20或100。但实际可能多个线程都成功。 }这个测试中withdraw方法不是原子操作我们又在读取和更新之间插入了随机休眠这使得多个线程更有可能都读到初始余额100并都认为自己可以取款80导致余额被透支为负数。5.2 使用CyclicBarrier精确控制并发执行点CountDownLatch是一次性的而CyclicBarrier循环屏障可以重复使用它能让一组线程互相等待直到所有线程都到达某个屏障点然后一起继续执行。这对于测试需要严格同时触发的并发场景非常有用。Test void testCacheStampedeWithBarrier() throws Exception { // 模拟缓存击穿场景多个线程同时发现缓存失效同时去加载数据库 class CacheLoader { private String cache null; private int loadCount 0; // 记录实际加载数据库的次数 public String getData() { if (cache null) { // 模拟加载数据库耗时操作 synchronized (this) { // 双重检查锁定 (DCL) if (cache null) { System.out.println(Thread.currentThread().getName() 开始加载数据库...); loadCount; try { Thread.sleep(100); } catch (InterruptedException e) {} // 模拟IO cache Data from DB; } } } return cache; } public int getLoadCount() { return loadCount; } } int threadCount 10; CacheLoader loader new CacheLoader(); ExecutorService executor Executors.newFixedThreadPool(threadCount); // 创建一个CyclicBarrier等待所有线程就绪 CyclicBarrier barrier new CyclicBarrier(threadCount 1); // 1 包括主线程 for (int i 0; i threadCount; i) { executor.submit(() - { try { // 所有线程在此等待 barrier.await(); // 同时开始执行getData() loader.getData(); } catch (Exception e) { e.printStackTrace(); } }); } System.out.println(所有线程已提交准备同时触发...); Thread.sleep(100); // 给线程一点时间到达屏障 // 主线程到达屏障释放所有等待的线程 barrier.await(); executor.shutdown(); executor.awaitTermination(2, TimeUnit.SECONDS); System.out.println(数据库实际加载次数: loader.getLoadCount()); // 在DCL正确的实现下loadCount应为1。但如果缺少volatile或指令重排可能大于1。 assertEquals(1, loader.getLoadCount()); }注意事项使用CyclicBarrier时屏障数量parties参数一定要设置正确包括所有的工作线程有时还需要包括主控线程。如果有一个线程因为异常没有到达屏障或者屏障数量对不上所有其他线程都会永远等待下去导致测试挂起。务必设置合理的超时时间barrier.await(5, TimeUnit.SECONDS)或在测试框架中管理超时。6. 方法四集成测试中的并发场景模拟——以Spring Boot与TestContainers为例单元测试能覆盖类内部的并发问题但一些并发缺陷只有在集成环境中当多个组件如应用服务、数据库、缓存交互时才会显现。比如数据库事务隔离级别设置不当导致的数据不一致。这里我们结合Spring Boot和TestContainers搭建一个更贴近真实环境的并发集成测试。6.1 模拟并发下单导致的超卖问题超卖是电商系统经典的并发问题商品库存为1两个用户同时下单都成功扣减库存导致卖了2件商品。1. 准备测试环境使用H2内存数据库和TestContainers for Redis首先在pom.xml中引入依赖以Maven为例dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency dependency groupIdorg.testcontainers/groupId artifactIdtestcontainers/artifactId version1.19.3/version !-- 使用最新版本 -- scopetest/scope /dependency dependency groupIdorg.testcontainers/groupId artifactIdjunit-jupiter/artifactId version1.19.3/version scopetest/scope /dependency2. 编写有缺陷的库存扣减服务Service public class InventoryService { Autowired private JdbcTemplate jdbcTemplate; // 存在并发问题的扣减方法先查后改 Transactional public boolean deductStock(Long productId, Integer quantity) { // 1. 查询当前库存 Integer currentStock jdbcTemplate.queryForObject( SELECT stock FROM product WHERE id ?, Integer.class, productId); if (currentStock null || currentStock quantity) { return false; } // 2. 模拟一些业务逻辑处理耗时 try { Thread.sleep(10); } catch (InterruptedException e) {} // 3. 更新库存 int rows jdbcTemplate.update( UPDATE product SET stock stock - ? WHERE id ?, quantity, productId); return rows 0; } }3. 编写并发集成测试SpringBootTest Testcontainers class InventoryServiceConcurrentTest { Autowired private InventoryService inventoryService; Container static PostgreSQLContainer? postgres new PostgreSQLContainer(postgres:15-alpine); DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add(spring.datasource.url, postgres::getJdbcUrl); registry.add(spring.datasource.username, postgres::getUsername); registry.add(spring.datasource.password, postgres::getPassword); } BeforeEach void setup() { // 初始化数据库插入一条库存为10的商品 jdbcTemplate.execute(DROP TABLE IF EXISTS product); jdbcTemplate.execute(CREATE TABLE product (id BIGINT PRIMARY KEY, stock INT)); jdbcTemplate.update(INSERT INTO product (id, stock) VALUES (1, 10)); } Test void testDeductStockConcurrencyProblem() throws InterruptedException { int threadCount 15; int deductPerThread 1; CountDownLatch startLatch new CountDownLatch(1); CountDownLatch endLatch new CountDownLatch(threadCount); AtomicInteger successCount new AtomicInteger(0); ExecutorService executor Executors.newFixedThreadPool(threadCount); for (int i 0; i threadCount; i) { executor.submit(() - { try { startLatch.await(); boolean success inventoryService.deductStock(1L, deductPerThread); if (success) { successCount.incrementAndGet(); } } catch (Exception e) { e.printStackTrace(); } finally { endLatch.countDown(); } }); } startLatch.countDown(); endLatch.await(); executor.shutdown(); // 查询最终库存 Integer finalStock jdbcTemplate.queryForObject( SELECT stock FROM product WHERE id 1, Integer.class); System.out.println(成功扣减的线程数: successCount.get()); System.out.println(数据库最终库存: finalStock); // 断言成功次数 最终库存 应该等于初始库存 // 初始库存10每个线程扣1最多成功10次库存为0。 // 但由于并发问题可能出现成功次数10库存为负数的情况。 assertEquals(10, successCount.get() finalStock); } }6.2 分析与解决方案运行上述测试你很可能会发现断言失败successCount.get() finalStock不等于10。这是因为deductStock方法存在“先查后改”的竞态条件。在READ COMMITTED读已提交PostgreSQL默认或REPEATABLE READ隔离级别下两个事务可能同时读到库存为10都判断为足够然后都去扣减导致超卖。解决方案1使用数据库悲观锁SELECT FOR UPDATETransactional public boolean deductStockPessimistic(Long productId, Integer quantity) { // 使用悲观锁锁定这行数据 Integer currentStock jdbcTemplate.queryForObject( SELECT stock FROM product WHERE id ? FOR UPDATE, Integer.class, productId); // ... 后续逻辑不变 }解决方案2使用数据库乐观锁版本号或条件更新Transactional public boolean deductStockOptimistic(Long productId, Integer quantity) { // 直接使用原子性更新操作 int rows jdbcTemplate.update( UPDATE product SET stock stock - ? WHERE id ? AND stock ?, quantity, productId, quantity); return rows 0; // 返回更新行数大于0表示成功 }解决方案3在应用层使用分布式锁对于分布式环境可以使用Redis或ZooKeeper实现分布式锁确保同一时间只有一个实例可以执行扣减逻辑。集成测试心得使用TestContainers可以轻松启动真实的外部服务如PostgreSQL, Redis, Kafka让集成测试的环境与生产环境高度一致能发现更多依赖特定数据库行为或中间件特性的并发问题。但这类测试运行较慢更适合在CI/CD流水线中作为关键路径的验收测试而不是每次开发都运行。7. 并发测试中常见问题与排查技巧实录即使使用了上述方法并发问题依然可能难以复现和定位。下面分享一些我在实践中总结的排查技巧和工具。7.1 问题复现与日志排查并发问题日志往往杂乱无章关键信息被淹没。技巧1给日志加上唯一追踪标识在每个请求或任务开始时生成一个唯一的ID如UUID或TraceId并在线程上下文如ThreadLocal或MDC中传递。在所有的日志语句中都输出这个ID。这样你可以通过这个ID将分散在不同线程日志中的、属于同一个逻辑流程的消息串联起来。// 使用Slf4j MDC import org.slf4j.MDC; class ConcurrentService { public void process() { String traceId UUID.randomUUID().toString(); MDC.put(traceId, traceId); try { log.info(开始处理); // ... 业务逻辑 log.info(处理完成); } finally { MDC.clear(); // 务必清理防止内存泄漏 } } } // 在logback.xml中配置pattern: %d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n技巧2有选择地增加详细日志并使用Thread.currentThread().getName()在怀疑存在竞态条件的代码块前后打印关键变量的状态和线程名。public void suspiciousMethod(SharedObject obj) { log.debug(Thread[{}] - 进入方法obj.state{}, Thread.currentThread().getName(), obj.getState()); // ... 危险操作 log.debug(Thread[{}] - 操作完成obj.state{}, Thread.currentThread().getName(), obj.getState()); }7.2 利用JVM工具与Java诊断工具当日志无法定位时需要借助更强大的工具。1.jstack查看线程状态和锁持有情况jstack是JDK自带的命令行工具可以抓取JVM所有线程的堆栈信息。当应用疑似发生死锁时这是第一选择。# 找到Java进程的PID jps -l # 生成线程转储 jstack pid thread_dump.txt在thread_dump.txt中搜索“deadlock”或“BLOCKED”状态可以快速找到相互等待锁的线程。2. 使用jconsole或VisualVM进行可视化监控这些工具可以实时查看线程状态、内存使用、监视器锁信息。VisualVM的“线程”标签页可以直观地看到哪些线程在运行、等待、阻塞并且可以手动执行垃圾回收或进行线程转储。3. 使用-XX:PrintConcurrentLocks和-XX:PrintSafepointStatistics等JVM参数在启动应用时添加这些参数可以输出更详细的锁竞争和JVM安全点信息帮助分析由垃圾回收GC引起的“Stop-The-World”导致的并发停顿问题。7.3 编写可测试的并发代码最好的排查是预防。在编写代码时就为并发测试做好准备。原则1缩小同步范围尽量只锁住必要的共享数据而不是整个方法。使用更细粒度的锁对象。// 不好 public synchronized void processEverything() { ... } // 更好 private final Object specificLock new Object(); public void process() { // 非同步部分... synchronized (specificLock) { // 只同步真正需要保护的一小段代码 } // 非同步部分... }原则2优先使用并发工具类而非手动synchronizedjava.util.concurrent包提供了丰富且高性能的并发工具如ConcurrentHashMap、CopyOnWriteArrayList、AtomicInteger、CountDownLatch、CyclicBarrier、Semaphore等。它们经过了充分的测试和优化比自己实现的同步逻辑更可靠。原则3避免在同步块中调用外部方法“外星方法”这可能导致死锁或性能问题因为你无法控制外部方法的行为。synchronized(lock) { // 不要这样做 someExternalService.call(); // 这个外部服务可能也在获取锁导致死锁 list.add(item); }原则4使用不可变对象和线程封闭最简单的线程安全策略就是避免共享。尽可能使用不可变对象所有字段final状态在构造后不变或者使用ThreadLocal将对象封闭在单个线程内。并发测试是一场与不确定性的战斗。没有一种方法能保证发现所有问题但通过组合运用单元测试、集成测试、代码审查和静态分析工具如FindBugs、SpotBugs、Error Prone我们可以将并发缺陷的风险降到最低。记住面对并发永远要保持敬畏之心。在你认为“这怎么可能出错”的地方往往就是bug藏身之处。
返回列表