Java多线程并发案例实操:从理论到项目的完整实战指南
目录导读
- 为什么实战案例是掌握并发的关键?
- 基础案例:线程创建与生命周期管理
- 进阶案例:锁与同步机制的正确使用
- 高级案例:线程池、并发集合与CompletableFuture
- 典型面试问答:并发场景下的陷阱与解决
- 从案例中提炼的3个核心原则
为什么实战案例是掌握并发的关键?
许多开发者学习Java多线程时,能理解Thread、Runnable、synchronized的概念,但一到真实项目就手足无措。案例实操的价值在于:将抽象理论转化为可运行的代码,暴露并发中的典型陷阱(如死锁、可见性问题、线程饥饿),并通过调试与优化形成肌肉记忆。

本篇文章精选3个不同层级的案例,覆盖从基础到生产级场景,所有代码均可在JDK 8+环境下直接运行。
基础案例:线程创建与生命周期管理
案例描述
模拟一个银行账户的存款与取款操作,要求两个线程同时操作同一账户,最终账户余额正确。
错误示范(不操实战会踩的坑)
class BankAccount {
private int balance = 100;
public void withdraw(int amount) {
if (balance >= amount) {
// 模拟网络延迟
try { Thread.sleep(10); } catch (InterruptedException e) {}
balance -= amount;
System.out.println("取款成功,余额:" + balance);
}
}
}
问题:线程A判断余额足够后进入sleep,线程B同样判断通过,导致余额被扣两次,变为负数。
正确实操方式
class SafeBankAccount {
private int balance = 100;
public synchronized void withdraw(int amount) {
if (balance >= amount) {
try { Thread.sleep(10); } catch (InterruptedException e) {}
balance -= amount;
System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + balance);
}
}
}
关键点:使用synchronized将方法变成原子操作,确保判断与修改不可分割。
实操指令
- 创建两个线程分别执行取款50元操作。
- 观察控制台输出,验证余额是否保持为0。
- 尝试将
synchronized去掉,观察经典的“脏读”现象。
进阶案例:锁与同步机制的正确使用
案例描述
任务调度系统:多个线程同时处理任务,但同一时间只能有N个线程执行(例如只允许3个线程同时计算)。
使用ReentrantLock+Condition实现限流
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
class TaskScheduler {
private final int maxConcurrent = 3;
private int currentCount = 0;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
public void executeTask() throws InterruptedException {
lock.lock();
try {
while (currentCount >= maxConcurrent) {
System.out.println(Thread.currentThread().getName() + "等待中...");
notFull.await(); // 释放锁并阻塞
}
currentCount++;
System.out.println(Thread.currentThread().getName() + "执行任务,当前并发:" + currentCount);
} finally {
lock.unlock();
}
// 模拟任务执行
Thread.sleep(100);
// 完成时释放槽位
lock.lock();
try {
currentCount--;
notFull.signalAll(); // 唤醒等待线程
} finally {
lock.unlock();
}
}
}
实操技巧
- 避免
synchronized的局限性:当需要超时等待、可中断锁、公平锁时,必须使用ReentrantLock。 Condition的精准唤醒:比synchronized的wait/notify更灵活,支持多条件队列。
高级案例:线程池、并发集合与CompletableFuture
案例描述
电商批量查询价格:需要同时调用5个微服务获取不同商品的价格,等待所有结果返回后合并展示。
使用ExecutorService+CompletableFuture实现
import java.util.concurrent.*;
import java.util.*;
public class PriceAggregator {
private final ExecutorService pool = Executors.newFixedThreadPool(3);
public CompletableFuture<Double> queryPrice(String product) {
return CompletableFuture.supplyAsync(() -> {
// 模拟耗时服务调用
try { Thread.sleep(200); } catch (InterruptedException e) {}
if (product.equals("手机")) return 4999.0;
if (product.equals("电脑")) return 8999.0;
return 100.0;
}, pool);
}
public static void main(String[] args) throws Exception {
PriceAggregator aggregator = new PriceAggregator();
List<String> products = Arrays.asList("手机", "电脑", "耳机");
// 并发查询所有价格
List<CompletableFuture<Double>> futures = products.stream()
.map(aggregator::queryPrice)
.collect(Collectors.toList());
// 等待所有完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
allFutures.join();
// 提取结果
List<Double> prices = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
System.out.println("最终价格:" + prices); // [4999.0, 8999.0, 100.0]
aggregator.pool.shutdown();
}
}
实战要点
- 线程池大小:I/O密集型可设置
2 * CPU核心数,计算密集型设置CPU核心数 + 1。 - 并发集合优先:使用
ConcurrentHashMap而非HashMap,使用CopyOnWriteArrayList替代ArrayList(写少读多场景)。 - 异常处理:
CompletableFuture通过exceptionally或handle优雅处理异常,避免漏掉。
典型面试问答:并发场景下的陷阱与解决
Q1:如何避免死锁?
A:按固定顺序加锁(例如所有线程先锁A再锁B);使用带超时的方法tryLock(3, TimeUnit.SECONDS);使用锁排序工具如LockSupport。
Q2:volatile能保证原子性吗?
A:不能。volatile只保证可见性和有序性,对count++这样复合操作仍需AtomicInteger或synchronized。
Q3:线程池的拒绝策略有什么实际用途?
A:CallerRunsPolicy让提交线程自己运行任务(反压机制),DiscardPolicy丢弃任务,适用于丢失不影响核心流程。
从案例中提炼的3个核心原则
- 原子性第一:任何涉及共享数据的修改,必须用锁或原子类包装。
- 线程池代替裸线程:避免频繁创建/销毁线程,使用
Executors或自定义参数。 - 异步编排用CompletableFuture:替代漫长的
CountDownLatch和Future.get,可读性更强。
最终建议:将上述代码复制到IDE中,修改参数(如增减线程数、调整sleep时间),观察并发带来的数据不一致、卡顿等现象,这才是真正的“实操”。