多线程
CountDownLatch 与 CyclicBarrier 的区别及使用场景
在 Java 多线程编程中,CountDownLatch
和 CyclicBarrier
是两种不同的同步工具:
- 主要区别
- CountDownLatch:是一个计数器,初始时设置一个值,线程通过调用
countDown()
方法将计数器减 1,其他线程可以调用await()
方法等待计数器变为 0。计数器一旦变为 0 就不能再重置,适用于一个或多个线程等待其他线程完成操作的场景。 - CyclicBarrier:让一组线程在到达屏障点时互相等待,所有线程都到达屏障点后,屏障才会打开,线程继续执行。它可以重复使用(通过
reset()
方法),适用于多个线程需要相互等待,共同到达某个状态后再继续执行的场景。
- CountDownLatch:是一个计数器,初始时设置一个值,线程通过调用
- 项目中的使用案例
- CountDownLatch:在分布式系统中,主线程需要等待多个子线程完成数据加载或处理后才能继续执行后续操作。
- CyclicBarrier:在并行计算中,多个线程分工计算不同部分的数据,全部计算完成后,再进行汇总和下一步处理。
以下是一个使用 CountDownLatch
的简单示例:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int taskId = i;
new Thread(() -> {
System.out.println("Task " + taskId + " started");
try {
Thread.sleep(1000); // 模拟任务执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task " + taskId + " completed");
latch.countDown(); // 任务完成,计数器减1
}).start();
}
// 主线程等待所有任务完成
latch.await();
System.out.println("All tasks completed, main thread continues");
}
}
以下是一个使用 CyclicBarrier
的简单示例:下面是一个使用 CyclicBarrier
的典型案例,模拟多个运动员参加跑步比赛。所有运动员准备好后才能同时起跑,到达终点后可以进行下一轮比赛。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierRaceExample {
public static void main(String[] args) {
int athleteCount = 3;
// 创建 CyclicBarrier,指定屏障解除时执行的动作
CyclicBarrier barrier = new CyclicBarrier(athleteCount,
() -> System.out.println("所有运动员已准备就绪,比赛开始!"));
System.out.println("===== 第一轮比赛 =====");
for (int i = 0; i < athleteCount; i++) {
final int athleteId = i + 1;
new Thread(() -> {
try {
System.out.println("运动员 " + athleteId + " 正在准备...");
Thread.sleep((long) (Math.random() * 3000)); // 模拟准备时间
System.out.println("运动员 " + athleteId + " 已准备好,等待其他运动员");
barrier.await(); // 等待其他运动员
System.out.println("运动员 " + athleteId + " 开始跑步");
Thread.sleep((long) (Math.random() * 5000)); // 模拟跑步时间
System.out.println("运动员 " + athleteId + " 到达终点");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
System.err.println("运动员 " + athleteId + " 比赛中断: " + e.getMessage());
}
}).start();
}
// 模拟可以进行下一轮比赛
try {
Thread.sleep(10000);
System.out.println("\n===== 第二轮比赛 =====");
// 这里可以重新启动新的线程使用同一个 barrier 进行下一轮比赛
// 实际应用中可能会使用线程池来管理线程
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
-
创建 CyclicBarrier:
CyclicBarrier barrier = new CyclicBarrier(athleteCount, () -> System.out.println("所有运动员已准备就绪,比赛开始!"));
athleteCount
表示需要等待的线程数量(运动员数量)。- 第二个参数是一个 Runnable,当所有线程到达屏障时会执行这个 Runnable(宣布比赛开始)。
-
运动员准备阶段:
每个运动员线程模拟准备时间,然后调用barrier.await()
等待其他运动员。 -
屏障解除条件:
当所有运动员线程都调用了await()
,屏障解除,执行构造函数中指定的 Runnable,然后所有线程继续执行后续代码(开始跑步)。 -
可重用性:
一轮比赛结束后,CyclicBarrier
可以重置并用于下一轮比赛(示例中通过注释演示了这一点)。
CyclicBarrier
适用于以下场景:
- 多线程任务需要分阶段执行,每个阶段都需要所有线程完成后才能继续。
- 并行计算中,多个子任务计算完成后,需要汇总结果再进行下一步。
- 循环执行的协作任务,例如多线程游戏中的回合制操作。
Java Stream 并行流(parallelStream)的优缺点及使用案例
优点
- 利用多核 CPU:在多核处理器上,并行流可以自动将数据分成多个块,每个块由不同的线程处理,显著提高处理大量数据的速度。
- 代码简洁:与手动编写多线程代码相比,并行流只需将
stream()
改为parallelStream()
,无需显式管理线程池和任务分配。 - 惰性求值:并行流与串行流一样支持惰性求值,中间操作不会立即执行,减少不必要的计算。
缺点
- 线程开销:创建和管理线程池需要额外开销,对于小规模数据或简单操作,并行流可能比串行流更慢。
- 线程安全风险:如果并行流操作共享可变状态,可能导致数据竞争和不一致结果。
- 顺序性问题:并行流不保证处理顺序,若业务逻辑依赖元素顺序(如分页、排序),需谨慎使用。
- 调试困难:多线程环境下的错误更难重现和调试。
使用案例
- 大规模数据处理:适用于元素数量大、处理逻辑复杂的场景。
import java.util.Arrays;
import java.util.List;
public class ParallelStreamExample {
public static void main(String[] args) {
// 模拟1000个元素的列表
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 并行计算所有元素的平方和
long sum = numbers.parallelStream()
.mapToLong(num -> {
// 模拟耗时操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return (long) num * num;
})
.sum();
System.out.println("平方和: " + sum);
}
}
- 独立元素处理:每个元素的处理不依赖其他元素的结果。
// 并行下载多个URL内容
List<String> urls = Arrays.asList("url1", "url2", "url3");
urls.parallelStream()
.map(url -> downloadContent(url)) // 假设downloadContent是线程安全的
.forEach(content -> processContent(content));
- 数值计算:适合并行的聚合操作(如求和、最大值)。
// 并行计算平均值
double average = numbers.parallelStream()
.mapToInt(Integer::intValue)
.average()
.orElse(0);
性能优化建议
- 避免共享可变状态:确保并行流操作不修改共享变量。
// 错误示例(共享可变状态)
List<Integer> results = new ArrayList<>();
numbers.parallelStream()
.map(num -> num * num)
.forEach(results::add); // 线程不安全
// 正确示例(使用collect)
List<Integer> results = numbers.parallelStream()
.map(num -> num * num)
.collect(Collectors.toList()); // 线程安全
- 使用并行友好的收集器:
Collectors.toConcurrentMap()
、Collectors.toConcurrentSet()
等。
// 并行收集到ConcurrentMap
Map<Integer, Integer> squareMap = numbers.parallelStream()
.collect(Collectors.toConcurrentMap(
num -> num,
num -> num * num
));
-
避免阻塞操作:并行流中的 I/O 操作可能导致线程长时间等待,建议使用异步 API(如 CompletableFuture)。
-
手动配置并行度:通过
ForkJoinPool.commonPool()
调整线程池大小(不推荐频繁使用)。
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4");
适用场景总结
- 推荐使用:数据量大、处理逻辑复杂、元素处理独立、多核 CPU 环境。
- 不推荐使用:数据量小、操作简单、依赖顺序处理、涉及 I/O 或同步操作。
在多线程环境下,如果有多个线程同时访问并修改共享资源,可能会引发线程安全问题。假设你负责一个银行转账的功能实现,多个线程同时进行转账操作,你会如何保证账户余额的准确性,避免出现超转等问题?请具体说说你会采用哪些同步机制或工具类。
银行转账功能的线程安全实现方案
在多线程环境下处理银行转账这类共享资源操作时,我会采用以下策略确保线程安全:
使用 synchronized 关键字实现基本同步
这是最基础的同步方式,直接在转账方法上加锁:
public class Account {
private double balance;
public synchronized void transfer(Account target, double amount) {
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
} else {
throw new IllegalArgumentException("余额不足");
}
}
}
优点:实现简单
缺点:
- 锁粒度大,整个方法被锁定
- 可能导致死锁(例如双向转账时)
在并发场景中,双向转账(如账户 A 向账户 B 转账,同时账户 B 向账户 A 转账)是典型的可能导致死锁的场景。下面从 死锁产生原因、代码示例 和 解决方案 三个方面详细说明。
一、双向转账死锁的产生原因
死锁的产生需要满足四个必要条件:互斥条件、持有并等待、不可剥夺、循环等待。双向转账场景中,这四个条件会同时满足,最终导致死锁。
以 “账户 A 向 B 转账 100 元” 和 “账户 B 向 A 转账 200 元” 两个并发操作为例:
- 互斥条件:每个账户的余额更新需要加锁(同一时间只能有一个线程操作),满足互斥。
- 持有并等待:线程 1 先锁定 A 账户,再等待锁定 B 账户;线程 2 先锁定 B 账户,再等待锁定 A 账户,满足 “持有一个锁并等待另一个锁”。
- 不可剥夺:线程持有的锁不会被其他线程强制剥夺,满足不可剥夺。
- 循环等待:线程 1 持有 A 锁等待 B 锁,线程 2 持有 B 锁等待 A 锁,形成循环等待链,满足循环等待。
四个条件同时满足,导致死锁:两个线程永远阻塞,无法继续执行。
二、代码示例:双向转账死锁重现
下面的代码模拟双向转账场景,展示死锁的产生过程:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; // 账户类 class Account { private final String id; // 账户唯一标识(如ID) private int balance; // 余额 private final Lock lock = new ReentrantLock(); // 账户锁 public Account(String id, int balance) { this.id = id; this.balance = balance; } // 转账方法:从当前账户转出amount到target账户 public void transfer(Account target, int amount) throws InterruptedException { // 先锁定当前账户 lock.lockInterruptibly(); try { // 再尝试锁定目标账户 target.lock.lockInterruptibly(); try { // 检查余额是否充足 if (this.balance >= amount) { // 执行转账 this.balance -= amount; target.balance += amount; System.out.println(Thread.currentThread().getName() + " 转账成功:" + this.id + " -> " + target.id + ",金额:" + amount); } else { System.out.println(Thread.currentThread().getName() + " 转账失败:" + this.id + "余额不足"); } } finally { target.lock.unlock(); // 释放目标账户锁 } } finally { lock.unlock(); // 释放当前账户锁 } } public String getId() { return id; } public int getBalance() { return balance; } } // 测试类 public class TransferDeadlock { public static void main(String[] args) throws InterruptedException { Account a = new Account("A", 1000); Account b = new Account("B", 1000); // 线程1:A向B转账200元 Thread t1 = new Thread(() -> { try { a.transfer(b, 200); } catch (InterruptedException e) { e.printStackTrace(); } }, "线程1"); // 线程2:B向A转账300元 Thread t2 = new Thread(() -> { try { b.transfer(a, 300); } catch (InterruptedException e) { e.printStackTrace(); } }, "线程2"); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("最终余额:A=" + a.getBalance() + ",B=" + b.getBalance()); } }
可能的执行结果:
程序可能永远卡住(死锁),不会输出 “最终余额”。因为:
- 线程 1 先获取 A 的锁,等待 B 的锁;
- 线程 2 先获取 B 的锁,等待 A 的锁;
- 两者互相等待,形成死锁。
三、解决双向转账死锁的方案
解决死锁的核心是 打破死锁的四个必要条件之一。由于互斥(账户锁必须互斥)、不可剥夺(锁不能被强制剥夺)通常无法避免,实际中主要通过 打破 “持有并等待” 或 “循环等待” 来解决。
方案 1:固定加锁顺序(打破循环等待)
循环等待的根源是两个线程加锁顺序相反(线程 1:A→B,线程 2:B→A)。若让所有线程 按照统一的顺序加锁(如先锁 ID 较小的账户,再锁 ID 较大的),则不会出现循环等待。
修改
transfer
方法,先比较两个账户的唯一标识(如 ID),总是先锁定标识 “更小” 的账户:public void transfer(Account target, int amount) throws InterruptedException { // 确定加锁顺序:先锁id较小的账户,再锁id较大的 Account firstLock = this.id.compareTo(target.id) < 0 ? this : target; Account secondLock = this.id.compareTo(target.id) > 0 ? this : target; // 按固定顺序加锁 firstLock.lock.lockInterruptibly(); try { secondLock.lock.lockInterruptibly(); try { if (this.balance >= amount) { this.balance -= amount; target.balance += amount; System.out.println(Thread.currentThread().getName() + " 转账成功:" + this.id + " -> " + target.id + ",金额:" + amount); } else { System.out.println(Thread.currentThread().getName() + " 转账失败:" + this.id + "余额不足"); } } finally { secondLock.lock.unlock(); } } finally { firstLock.lock.unlock(); } }
原理:
- 无论转账方向是 A→B 还是 B→A,加锁顺序都是 “先锁 ID 小的账户,再锁 ID 大的”(假设 A 的 ID 小于 B)。
- 线程 1 和线程 2 都会先尝试获取 A 的锁,再获取 B 的锁,避免了循环等待。
方案 2:使用 tryLock 超时机制(打破持有并等待)
tryLock(long timeout, TimeUnit unit)
可以在指定时间内尝试获取锁,若超时未获取则释放已持有的锁,避免 “持有一个锁并无限等待另一个锁”。修改
transfer
方法,使用tryLock
并设置超时时间,若获取不到目标锁则释放已持有的锁并重试:public boolean transfer(Account target, int amount) throws InterruptedException { // 先获取当前账户的锁 if (!this.lock.tryLock(1, TimeUnit.SECONDS)) { System.out.println(Thread.currentThread().getName() + " 获取" + this.id + "锁超时"); return false; } try { // 尝试获取目标账户的锁(超时1秒) if (!target.lock.tryLock(1, TimeUnit.SECONDS)) { System.out.println(Thread.currentThread().getName() + " 获取" + target.id + "锁超时,释放" + this.id + "锁"); return false; // 释放当前锁(由finally保证),重试 } try { if (this.balance >= amount) { this.balance -= amount; target.balance += amount; System.out.println(Thread.currentThread().getName() + " 转账成功:" + this.id + " -> " + target.id + ",金额:" + amount); return true; } else { System.out.println(Thread.currentThread().getName() + " 转账失败:" + this.id + "余额不足"); return false; } } finally { target.lock.unlock(); } } finally { this.lock.unlock(); } }
使用时需配合重试机制:
若一次转账失败(获取锁超时),可通过循环重试直到成功:// 线程1的任务改为循环重试 Thread t1 = new Thread(() -> { boolean success = false; while (!success) { try { success = a.transfer(b, 200); } catch (InterruptedException e) { e.printStackTrace(); break; } } }, "线程1");
原理:
- 若线程 1 持有 A 锁但获取 B 锁超时,则释放 A 锁,避免 “持有 A 等 B”;
- 线程 2 可能此时获取到 A 和 B 的锁完成转账,之后线程 1 重试时可顺利获取锁。
方案 3:使用更高层次的并发工具(如
AtomicReference
或无锁编程)若账户余额可以用原子类管理(如
AtomicInteger
),可通过无锁的 CAS 操作避免使用显式锁,从根本上消除死锁风险。但需注意 CAS 的 ABA 问题和逻辑复杂性。示例:
class Account { private final String id; private final AtomicInteger balance; // 原子余额 public Account(String id, int balance) { this.id = id; this.balance = new AtomicInteger(balance); } // 转账:通过CAS确保原子性(简化逻辑,实际需考虑重试) public boolean transfer(Account target, int amount) { int current = balance.get(); if (current < amount) { return false; // 余额不足 } // 尝试扣减当前账户余额 if (balance.compareAndSet(current, current - amount)) { // 增加目标账户余额 target.balance.addAndGet(amount); System.out.println(Thread.currentThread().getName() + " 转账成功:" + this.id + " -> " + target.id); return true; } return false; // CAS失败,需重试 } }
原理:
- 用
AtomicInteger
的compareAndSet
实现无锁的余额更新,避免使用显式锁,自然不会产生死锁。- 但需处理 CAS 失败的重试逻辑,适合简单场景。
四、总结
双向转账死锁的核心原因是 线程间加锁顺序相反导致的循环等待。实际开发中,固定加锁顺序 是最常用、最简单的解决方案(方案 1),它通过统一加锁顺序打破循环等待,逻辑清晰且性能稳定。
若加锁顺序难以固定(如账户无唯一标识),可采用 tryLock 超时 + 重试(方案 2),但需注意重试频率避免资源浪费。无锁编程(方案 3)适合简单场景,复杂业务中逻辑成本较高。
细粒度锁 - 使用 ReentrantLock
通过获取两个账户的锁来保证操作的原子性:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private double balance;
private final Lock lock = new ReentrantLock();
public void transfer(Account target, double amount) {
// 通过哈希值确定加锁顺序,避免死锁
Account first = this.hashCode() < target.hashCode() ? this : target;
Account second = this.hashCode() < target.hashCode() ? target : this;
first.lock.lock();
try {
second.lock.lock();
try {
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
} else {
throw new IllegalArgumentException("余额不足");
}
} finally {
second.lock.unlock();
}
} finally {
first.lock.unlock();
}
}
}
优点:
- 锁粒度更细,提高并发度
- 通过固定加锁顺序避免死锁
使用原子类 - AtomicIntegerFieldUpdater
如果账户类不可修改,可以使用原子更新器:
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class Account {
// 必须是volatile且非private
public volatile int balance;
private static final AtomicIntegerFieldUpdater<Account> UPDATER =
AtomicIntegerFieldUpdater.newUpdater(Account.class, "balance");
public void transfer(Account target, int amount) {
int srcBalance;
do {
srcBalance = UPDATER.get(this);
if (srcBalance < amount) {
throw new IllegalArgumentException("余额不足");
}
} while (!UPDATER.compareAndSet(this, srcBalance, srcBalance - amount));
// 这里需要确保目标账户的更新也原子化
UPDATER.addAndGet(target, amount);
}
}
优点:
- 无锁实现,性能更高
- 适用于高并发场景
数据库层面的乐观锁
在数据库表中添加版本号字段:
public class AccountService {
@Transactional
public void transfer(Long fromId, Long toId, double amount) {
Account fromAccount = accountDao.selectById(fromId);
if (fromAccount.getBalance() < amount) {
throw new IllegalArgumentException("余额不足");
}
int rows = accountDao.updateBalance(
fromId,
fromAccount.getBalance() - amount,
fromAccount.getVersion()
);
if (rows == 0) {
throw new OptimisticLockException("转账冲突,请重试");
}
accountDao.updateBalance(
toId,
accountDao.selectById(toId).getBalance() + amount,
accountDao.selectById(toId).getVersion()
);
}
}
对应的 SQL mapper:
<update id="updateBalance">
UPDATE account
SET balance = #{newBalance}, version = version + 1
WHERE id = #{id} AND version = #{version}
</update>
优点:
- 数据库层面保证一致性
- 适合分布式系统
分布式锁(适用于微服务架构)
使用 Redis 或 ZooKeeper 实现分布式锁:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TransferService {
@Autowired
private RedissonClient redissonClient;
public void transfer(String fromAccountId, String toAccountId, double amount) {
// 获取两个账户的锁(按固定顺序)
RLock lock1 = redissonClient.getLock("account:" + Math.min(fromAccountId.hashCode(), toAccountId.hashCode()));
RLock lock2 = redissonClient.getLock("account:" + Math.max(fromAccountId.hashCode(), toAccountId.hashCode()));
try {
lock1.lock();
try {
lock2.lock();
try {
// 执行转账操作
doTransfer(fromAccountId, toAccountId, amount);
} finally {
lock2.unlock();
}
} finally {
lock1.unlock();
}
} catch (Exception e) {
// 异常处理
}
}
}
优点:
- 跨服务保证线程安全
- 支持集群环境
使用 StampedLock (Java 8+)
在读多写少的场景下性能更优:
import java.util.concurrent.locks.StampedLock;
public class Account {
private double balance;
private final StampedLock lock = new StampedLock();
public void transfer(Account target, double amount) {
long stamp = lock.writeLock();
try {
if (balance >= amount) {
balance -= amount;
target.deposit(amount);
} else {
throw new IllegalArgumentException("余额不足");
}
} finally {
lock.unlockWrite(stamp);
}
}
private void deposit(double amount) {
long stamp = lock.writeLock();
try {
balance += amount;
} finally {
lock.unlockWrite(stamp);
}
}
}
优点:
- 比 ReentrantLock 性能更高
- 支持乐观读锁
选择策略总结
- 单机环境:优先使用 ReentrantLock 或 synchronized
- 高并发场景:考虑使用 AtomicIntegerFieldUpdater 或 StampedLock
- 数据库操作:结合数据库事务和乐观锁
- 分布式系统:使用 Redis 分布式锁或分布式事务
银行转账功能在多线程环境下可能会出现哪些线程安全问题?
在多线程环境下实现银行转账功能时,可能会遇到以下几类线程安全问题,这些问题会直接影响账户余额的准确性和系统一致性:
原子性破坏导致的余额错误
典型问题:超转(Overdraft)
- 场景:多个线程同时执行转账操作,读取相同余额后进行扣减,导致实际扣减金额超过可用余额。
- 示例:
- 账户余额为 1000 元,线程 A 和线程 B 同时发起 500 元转账。
- 线程 A 读取余额 1000,判断足够扣减;线程 B 同时读取余额 1000,也判断足够扣减。
- 最终余额可能变为 0(1000-500-500),但实际应只允许一笔转账成功,这就是超转。
部分操作失败导致的数据不一致
- 场景:转账操作分为 “扣减源账户” 和 “增加目标账户” 两步,若其中一步因异常中断,会导致余额失衡。
- 示例:
- 线程 A 扣减源账户后,未完成目标账户增加时系统崩溃,最终源账户少了钱,目标账户未到账。
可见性问题导致的脏读
缓存数据与主内存不一致
- 原因:Java 内存模型中,线程可能从工作内存读取过期的余额数据(未同步主内存更新)。
- 示例:
- 线程 A 完成转账并更新主内存余额为 500 元,但线程 B 的工作内存仍缓存着旧余额 1000 元,导致线程 B 继续基于旧数据操作。
未使用 volatile 或原子操作的字段更新
- 后果:非原子操作的余额更新(如
balance -= amount
)可能被 JVM 指令重排序,导致其他线程看到中间状态。
有序性问题引发的逻辑错误
指令重排序导致的异常判断失效
- 场景:余额检查和扣减操作被重排序,导致先扣减后检查。
- 示例
if (balance >= amount) { // 可能被重排序到扣减操作之后
balance -= amount;
}
若重排序发生,线程可能在余额不足时仍执行扣减,导致负数余额。
多步骤操作的顺序混乱
- 场景:转账时先更新目标账户,再更新源账户,若中间出错会导致资金凭空增加。
死锁与资源竞争问题
双向转账导致的死锁
- 场景:线程 A 尝试先锁账户 A 再锁账户 B,线程 B 尝试先锁账户 B 再锁账户 A,双方互相等待对方释放锁。
- 示例:
- 线程 A:lock (账户 A) → lock (账户 B) → 转账
- 线程 B:lock (账户 B) → lock (账户 A) → 转账
- 双方卡住,形成死锁。
锁粒度不当导致的性能瓶颈
- 场景:使用粗粒度锁(如
synchronized
修饰整个转账方法),导致大量线程阻塞等待。 - 后果:系统并发度降低,响应时间变长。
数据库层面的并发问题
幻读与不可重复读
- 场景:在数据库事务中,两次查询得到不同的余额结果,导致转账逻辑错误。
- 示例:
- 事务 A 查询余额为 1000 元,准备转账 500 元;
- 事务 B 同时完成转账 500 元,余额变为 500 元;
- 事务 A 继续执行扣减 500 元,导致余额变为 0,实际应因余额不足失败。
乐观锁冲突与重试问题
- 场景:多个线程同时基于旧版本号更新余额,导致部分线程更新失败(乐观锁失效)。
- 后果:需要额外处理重试逻辑,否则会丢失转账请求。
分布式环境下的特殊问题
跨服务转账的最终一致性问题
- 场景:微服务架构中,源账户和目标账户属于不同服务,分布式事务处理不当。
- 后果:可能出现 “源账户已扣减,目标账户未到账” 的情况(如网络超时)。
分布式锁失效
- 场景:Redis 分布式锁未正确实现(如锁超时、主从切换丢失锁),导致多线程同时操作。
如何避免这些问题?
- 原子性保障:使用
synchronized
、ReentrantLock
或原子类(AtomicLong
)包裹转账操作。 - 可见性保障:对余额字段使用
volatile
修饰,或通过锁强制内存同步。 - 有序性保障:避免指令重排序(如使用
final
或锁)。 - 死锁预防:固定加锁顺序(如按账户 ID 排序)、使用定时锁(
tryLock()
)。 - 数据库层面:使用事务(
@Transactional
)、乐观锁(版本号机制)。 - 分布式场景:引入分布式锁(Redis/ZooKeeper)、实现最终一致性(如消息队列重试)。
总结
银行转账的线程安全问题本质是多线程对共享资源(账户余额)的竞争修改问题,需从内存模型、锁机制、数据库事务、分布式系统等多个层面综合防护,避免因原子性、可见性、有序性缺失导致的资金错误,同时平衡并发性能与安全性。
在多线程环境下,如果需要实现线程按特定顺序执行,例如线程 A 执行完后线程 B 执行,线程 B 执行完后线程 C 执行,你会使用什么方法来达成这一目的?
使用 CountDownLatch 实现链式等待
核心思路:通过链式的计数器控制线程执行顺序,每个后续线程等待前一个线程的计数器归零。
import java.util.concurrent.CountDownLatch;
public class OrderedExecutionByCountDownLatch {
public static void main(String[] args) throws InterruptedException {
// 定义两个计数器,控制A→B→C的顺序
CountDownLatch latchAtoB = new CountDownLatch(1);
CountDownLatch latchBtoC = new CountDownLatch(1);
Thread threadA = new Thread(() -> {
System.out.println("线程A开始执行");
try {
Thread.sleep(1000); // 模拟任务执行
System.out.println("线程A执行完毕,通知线程B");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latchAtoB.countDown(); // 通知线程B可以执行
}
});
Thread threadB = new Thread(() -> {
try {
System.out.println("线程B等待线程A完成");
latchAtoB.await(); // 等待线程A完成
System.out.println("线程B开始执行");
Thread.sleep(500);
System.out.println("线程B执行完毕,通知线程C");
latchBtoC.countDown(); // 通知线程C可以执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread threadC = new Thread(() -> {
try {
System.out.println("线程C等待线程B完成");
latchBtoC.await(); // 等待线程B完成
System.out.println("线程C开始执行");
Thread.sleep(300);
System.out.println("线程C执行完毕");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 按顺序启动线程(实际执行顺序由计数器控制)
threadC.start();
threadB.start();
threadA.start();
// 等待所有线程完成
threadA.join();
threadB.join();
threadC.join();
}
}
优点:
- 实现简单,逻辑清晰
- 适合固定数量的线程顺序控制
缺点:
- 需为每对相邻线程创建独立计数器
- 不支持动态调整线程数量
使用 Phaser 实现阶段式控制
核心思路:利用 Phaser 的阶段(phase)机制,每个线程在指定阶段等待前一阶段完成。
import java.util.concurrent.Phaser;
public class OrderedExecutionByPhaser {
public static void main(String[] args) {
// 创建Phaser,初始注册3个线程(A/B/C)
Phaser phaser = new Phaser(3) {
// 重写到达屏障时的回调,可用于阶段切换日志
@Override
protected boolean onAdvance(int phase, int registeredParties) {
System.out.println("阶段 " + phase + " 完成,进入阶段 " + (phase + 1));
return registeredParties == 0; // 当所有线程离开时终止
}
};
Thread threadA = new Thread(() -> {
try {
System.out.println("线程A进入阶段0");
phaser.arriveAndAwaitAdvance(); // 等待阶段0所有线程到达
System.out.println("线程A开始执行");
Thread.sleep(1000);
System.out.println("线程A执行完毕,进入阶段1");
phaser.arriveAndAwaitAdvance(); // 等待阶段1所有线程到达
phaser.arriveAndDeregister(); // 离开Phaser,减少注册数
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread threadB = new Thread(() -> {
try {
System.out.println("线程B进入阶段0");
phaser.arriveAndAwaitAdvance(); // 等待阶段0
System.out.println("线程B等待阶段1(线程A完成)");
phaser.arriveAndAwaitAdvance(); // 等待阶段1
System.out.println("线程B开始执行");
Thread.sleep(500);
System.out.println("线程B执行完毕,进入阶段2");
phaser.arriveAndAwaitAdvance(); // 等待阶段2
phaser.arriveAndDeregister();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread threadC = new Thread(() -> {
try {
System.out.println("线程C进入阶段0");
phaser.arriveAndAwaitAdvance(); // 等待阶段0
System.out.println("线程C等待阶段2(线程B完成)");
phaser.arriveAndAwaitAdvance(); // 等待阶段2
System.out.println("线程C开始执行");
Thread.sleep(300);
System.out.println("线程C执行完毕");
phaser.arriveAndDeregister();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 启动线程(顺序不影响执行顺序,由Phaser控制)
threadC.start();
threadB.start();
threadA.start();
}
}
优点:
- 支持动态调整阶段和线程数量
- 可通过重写
onAdvance
方法自定义阶段切换逻辑 - 代码结构更紧凑
缺点:
- 实现逻辑较复杂
- 需要精确控制每个线程的阶段注册
使用 wait/notify 实现线程通信
核心思路:通过对象监视器实现线程间的通知机制,前一个线程完成后通知下一个线程。
public class OrderedExecutionByWaitNotify {
private static final Object lock = new Object();
// 标记当前允许执行的线程ID(1=A,2=B,3=C)
private static int currentThread = 1;
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程A等待执行权限");
while (currentThread != 1) {
lock.wait(); // 等待允许执行A
}
System.out.println("线程A开始执行");
Thread.sleep(1000);
System.out.println("线程A执行完毕,通知线程B");
currentThread = 2;
lock.notifyAll(); // 通知B可以执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
Thread threadB = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程B等待执行权限");
while (currentThread != 2) {
lock.wait(); // 等待允许执行B
}
System.out.println("线程B开始执行");
Thread.sleep(500);
System.out.println("线程B执行完毕,通知线程C");
currentThread = 3;
lock.notifyAll(); // 通知C可以执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
Thread threadC = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程C等待执行权限");
while (currentThread != 3) {
lock.wait(); // 等待允许执行C
}
System.out.println("线程C开始执行");
Thread.sleep(300);
System.out.println("线程C执行完毕");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
// 启动线程(顺序不影响执行顺序,由锁控制)
threadC.start();
threadB.start();
threadA.start();
// 等待所有线程完成
try {
threadA.join();
threadB.join();
threadC.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
优点:
- 底层机制,灵活性高
- 适合自定义复杂的顺序控制逻辑
缺点:
- 需手动管理锁和等待条件,易出错
- 代码可读性较差,维护成本高
使用 CompletableFuture 实现函数式顺序
核心思路:利用 CompletableFuture 的链式调用,前一个任务完成后触发下一个任务。
import java.util.concurrent.CompletableFuture;
public class OrderedExecutionByCompletableFuture {
public static void main(String[] args) throws InterruptedException {
// 创建初始CompletableFuture
CompletableFuture<Void> futureA = CompletableFuture.runAsync(() -> {
System.out.println("线程A开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("线程A执行完毕");
});
// 线程B依赖线程A完成
CompletableFuture<Void> futureB = futureA.thenRun(() -> {
System.out.println("线程B开始执行");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("线程B执行完毕");
});
// 线程C依赖线程B完成
CompletableFuture<Void> futureC = futureB.thenRun(() -> {
System.out.println("线程C开始执行");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("线程C执行完毕");
});
// 等待所有任务完成
futureC.get();
}
}
优点:
- 函数式编程风格,代码简洁
- 天然支持异步任务的顺序编排
- 可轻松处理异常和结果传递
缺点:
- 底层依赖 ForkJoinPool,不适合手动管理线程池的场景
方案对比与选择建议
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
CountDownLatch | 固定数量线程的简单顺序控制 | 实现简单,逻辑清晰 | 需链式创建多个计数器 |
Phaser | 动态阶段控制或多阶段任务 | 支持阶段管理,可动态调整 | 实现复杂度较高 |
wait/notify | 自定义复杂顺序逻辑或底层控制 | 灵活性高,可精细控制 | 易出错,代码可读性差 |
CompletableFuture | 异步任务的函数式顺序编排 | 代码简洁,支持异步和异常处理 | 依赖默认线程池,扩展性有限 |
推荐选择:
- 若需求简单,优先使用 CountDownLatch 或 CompletableFuture
- 若涉及多阶段动态控制,使用 Phaser
- 底层框架开发或特殊场景可考虑 wait/notify
假设你正在开发一个高并发的电商系统,其中商品库存的扣减操作是多线程同时进行的。为了确保库存数量不会出现负数(超卖情况),除了前面提到的锁机制,你还能想到哪些优化手段来提高并发性能,同时保证数据的准确性呢?比如,是否可以考虑使用乐观锁机制,以及如何在代码层面实现它?
乐观锁机制实现
核心思路:利用数据库版本号(Version)或时间戳实现 CAS(Compare-And-Swap)操作,减少锁竞争。
数据库层面实现乐观锁
在库存表中添加 version
字段,每次更新时检查版本号:
@Service
public class StockService {
@Autowired
private StockMapper stockMapper;
@Transactional
public boolean deductStock(Long productId, int quantity) {
// 1. 查询当前库存和版本
Stock stock = stockMapper.selectByProductId(productId);
if (stock.getQuantity() < quantity) {
return false; // 库存不足
}
// 2. 尝试更新库存,携带版本号作为条件
int rowsAffected = stockMapper.updateStock(
productId,
stock.getQuantity() - quantity,
stock.getVersion()
);
// 3. 判断是否更新成功(若版本号不匹配,rowsAffected=0)
return rowsAffected > 0;
}
}
// Mapper XML
<update id="updateStock">
UPDATE stock
SET quantity = #{newQuantity}, version = version + 1
WHERE product_id = #{productId}
AND version = #{version} -- 关键条件:版本号匹配
</update>
执行流程:
- 线程 A 和线程 B 同时查询库存(版本号为 1)
- 线程 A 先提交更新,版本号变为 2
- 线程 B 提交更新时,因版本号不匹配而失败,需重试
代码层面实现重试机制
当乐观锁更新失败时,通过重试逻辑提高成功率:
public boolean deductStockWithRetry(Long productId, int quantity, int maxRetries) {
int retries = 0;
while (retries < maxRetries) {
Stock stock = stockMapper.selectByProductId(productId);
if (stock.getQuantity() < quantity) {
return false;
}
int rowsAffected = stockMapper.updateStock(
productId,
stock.getQuantity() - quantity,
stock.getVersion()
);
if (rowsAffected > 0) {
return true; // 更新成功
}
retries++;
log.info("库存更新失败,重试第{}次", retries);
Thread.sleep(100); // 短暂休眠避免重试风暴
}
return false; // 达到最大重试次数仍失败
}
分段锁优化
核心思路:将库存拆分为多个分段(如 10 个),每个分段独立加锁,减少锁竞争。
public class StockService {
private final int SEGMENT_COUNT = 10;
private final ReentrantLock[] locks = new ReentrantLock[SEGMENT_COUNT];
public StockService() {
// 初始化分段锁
for (int i = 0; i < SEGMENT_COUNT; i++) {
locks[i] = new ReentrantLock();
}
}
public boolean deductStock(Long productId, int quantity) {
// 根据产品ID计算分段索引
int segmentIndex = (int) (productId % SEGMENT_COUNT);
ReentrantLock lock = locks[segmentIndex];
lock.lock();
try {
Stock stock = stockMapper.selectByProductId(productId);
if (stock.getQuantity() >= quantity) {
stockMapper.updateQuantity(productId, stock.getQuantity() - quantity);
return true;
}
return false;
} finally {
lock.unlock();
}
}
}
优点:并发度提升 10 倍(假设分 10 段)
缺点:实现复杂,需提前规划分段数量
原子类(AtomicInteger)本地扣减
核心思路:在 JVM 内存中维护库存副本,使用原子类快速扣减,批量同步到数据库。
public class StockService {
// 本地库存缓存(Map<产品ID, 本地库存>)
private final ConcurrentHashMap<Long, AtomicInteger> localStocks = new ConcurrentHashMap<>();
// 定时任务:每5秒同步一次本地库存到数据库
@Scheduled(fixedRate = 5000)
public void syncLocalStockToDb() {
for (Map.Entry<Long, AtomicInteger> entry : localStocks.entrySet()) {
Long productId = entry.getKey();
int localQuantity = entry.getValue().get();
stockMapper.syncLocalStock(productId, localQuantity);
}
}
public boolean deductStock(Long productId, int quantity) {
// 从数据库加载初始库存到本地
localStocks.computeIfAbsent(productId,
id -> new AtomicInteger(stockMapper.getStock(id)));
AtomicInteger stock = localStocks.get(productId);
int current;
int next;
do {
current = stock.get();
if (current < quantity) {
return false; // 库存不足
}
next = current - quantity;
} while (!stock.compareAndSet(current, next)); // CAS操作
// 异步同步到数据库(不影响当前请求)
CompletableFuture.runAsync(() ->
stockMapper.deductStock(productId, quantity));
return true;
}
}
优点:无锁化本地操作,性能极高
缺点:弱一致性,适合允许短暂库存不一致的场景
Redis 预扣库存
核心思路:利用 Redis 的原子操作(INCRBY)快速扣减库存,异步同步到数据库。
public class StockService {
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
public boolean deductStock(Long productId, int quantity) {
String stockKey = "stock:" + productId;
// Redis原子扣减库存
Long result = redisTemplate.opsForValue().increment(stockKey, -quantity);
if (result == null || result < 0) {
// 扣减失败,回滚操作
redisTemplate.opsForValue().increment(stockKey, quantity);
return false;
}
// 异步同步到数据库
CompletableFuture.runAsync(() ->
stockMapper.deductStock(productId, quantity));
return true;
}
// 初始化库存到Redis
public void initStockToRedis(Long productId, int quantity) {
redisTemplate.opsForValue().setIfAbsent("stock:" + productId, quantity);
}
}
优点:
- Redis 单线程原子操作,避免锁竞争
- 性能极高,支持万级 QPS
缺点:
- 需要维护 Redis 与数据库的一致性
- 库存回滚逻辑复杂
令牌桶限流
核心思路:限制并发扣减请求数量,保护数据库。
public class StockService {
// 每秒只允许100个请求访问库存
private final RateLimiter rateLimiter = RateLimiter.create(100.0);
public boolean deductStock(Long productId, int quantity) {
// 尝试获取令牌,超时100ms
if (!rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
return false; // 限流,直接拒绝
}
// 执行扣减逻辑
try (Connection conn = dataSource.getConnection()) {
// 数据库扣减操作
return executeStockDeduction(conn, productId, quantity);
} catch (SQLException e) {
log.error("数据库异常", e);
return false;
}
}
}
优点:
- 保护数据库不被击穿
- 平滑处理流量峰值
缺点:
- 可能导致部分请求被拒绝
- 需要根据系统性能调整限流参数
方案对比与选择建议
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
乐观锁 | 读多写少,冲突概率低 | 实现简单,无锁开销 | 冲突频繁时重试成本高 |
分段锁 | 库存量大,并发度高 | 并发度显著提升 | 实现复杂,需预分配分段 |
原子类本地扣减 | 允许短暂不一致,读极高并发 | 无锁化,性能极致 | 弱一致性,可能超卖 |
Redis 预扣库存 | 秒杀等高并发场景 | 支持极高并发,分布式友好 | 需维护数据一致性,实现复杂 |
令牌桶限流 | 保护数据库,防止击穿 | 稳定系统性能 | 可能拒绝部分请求 |
推荐组合方案:
- 日常场景:乐观锁 + 令牌桶限流
- 秒杀场景:Redis 预扣库存 + 数据库最终一致性
- 本地缓存优化:原子类本地扣减 + 定时同步
总结
高并发库存扣减需根据业务场景选择合适的优化方案,核心原则是:
- 减少锁粒度:分段锁、乐观锁
- 前置处理:Redis 预扣、令牌桶限流
- 异步化:本地扣减 + 异步同步
- 权衡一致性:根据业务容忍度选择强一致或最终一致
在多线程编程里,线程池是常用的技术。假如你要设计一个线程池来处理大量的异步任务,你会依据什么原则来设置线程池的核心线程数、最大线程数以及队列容量呢?另外,如果线程池中的任务执行出现异常,你通常会采用什么方式去捕获和处理这些异常?
线程池核心参数设置原则
核心线程数(corePoolSize)
- CPU 密集型任务:
corePoolSize = CPU核心数 + 1
原因:预留 1 个线程处理线程切换开销,避免 CPU 空闲 - IO 密集型任务:
corePoolSize = CPU核心数 × 2
或更高
原因:IO 等待时线程释放 CPU,更多线程可提高吞吐量 - 混合型任务:
拆分为独立的 CPU 和 IO 任务池,或通过Thread.sleep()
模拟 IO 等待调优 - 示例计算:
4 核 CPU,IO 等待占比 80%:
corePoolSize = 4 / (1 - 0.8) = 20
最大线程数(maximumPoolSize)
- 公式参考:
maxPoolSize = (CPU核心数 × 2) + 内存GB数
例:8 核 CPU + 16GB 内存 →maxPoolSize = 16 + 16 = 32
- 任务性质影响:
- 快速任务(< 10ms):
maxPoolSize
可设为corePoolSize
的 5-10 倍 - 慢速任务(> 100ms):
maxPoolSize
应接近corePoolSize
- 快速任务(< 10ms):
- 系统资源限制:
需考虑 JVM 堆外内存(线程栈默认 1MB / 线程),避免 OOM
队列容量(workQueue)
- 直接提交队列(SynchronousQueue):
- 适用于:任务提交速度与处理速度接近的场景
- 配置:
maxPoolSize
需足够大,避免拒绝策略触发
- 有界队列(ArrayBlockingQueue):
- 公式:
队列容量 = (任务平均处理时间 / 任务到达间隔) × 1.5
- 例:处理时间 50ms,到达间隔 20ms → 容量 ≈4
- 公式:
- 无界队列(LinkedBlockingQueue):
- 适用于:任务量波动大,但需避免 OOM 的场景
- 建议:设置合理上限(如
Integer.MAX_VALUE
的 1/10)
其他参数
- keepAliveTime:
非核心线程空闲存活时间,IO 密集型可设为 30-60 秒,CPU 密集型设为 5-10 秒 - 拒绝策略(RejectedExecutionHandler):
AbortPolicy
(默认):直接抛异常CallerRunsPolicy
:任务回退到调用线程执行DiscardOldestPolicy
:丢弃队列中最老的任务DiscardPolicy
:静默丢弃任务
线程池异常处理方案
通过 Future 捕获异常
ExecutorService executor = Executors.newFixedThreadPool(10);
try {
// 提交任务并获取Future
Future<String> future = executor.submit(() -> {
if (/* 条件 */) {
throw new BusinessException("任务失败");
}
return "处理结果";
});
// 阻塞获取结果,捕获异常
String result = future.get();
} catch (ExecutionException e) {
// 处理任务内异常
Throwable cause = e.getCause();
if (cause instanceof BusinessException) {
log.error("业务异常", cause);
} else {
log.error("系统异常", cause);
}
} catch (InterruptedException e) {
// 处理中断异常
Thread.currentThread().interrupt();
} finally {
executor.shutdown();
}
自定义 UncaughtExceptionHandler
ExecutorService executor = new ThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
// 自定义ThreadFactory
r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, ex) -> {
log.error("线程{}执行异常", thread.getName(), ex);
// 可在这里记录异常或重启线程
});
t.setName("custom-thread-" + t.getId());
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 提交无返回值任务
executor.execute(() -> {
throw new RuntimeException("执行中异常");
});
包装任务捕获异常
public class ExceptionHandlingTask implements Runnable {
private final Runnable task;
private final Logger logger;
public ExceptionHandlingTask(Runnable task, Logger logger) {
this.task = task;
this.logger = logger;
}
@Override
public void run() {
try {
task.run();
} catch (Throwable t) {
logger.error("任务执行异常", t);
// 可在这里进行补偿操作
}
}
}
// 使用示例
executor.execute(new ExceptionHandlingTask(() -> {
// 任务逻辑
}, log));
重写 beforeExecute/afterExecute
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)
) {
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
log.info("线程{}准备执行任务", t.getName());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t != null) {
log.error("任务执行异常", t);
}
}
};
处理拒绝策略中的异常
// 自定义拒绝策略
class LoggingRejectedExecutionHandler implements RejectedExecutionHandler {
private final Logger logger;
public LoggingRejectedExecutionHandler(Logger logger) {
this.logger = logger;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (!executor.isShutdown()) {
logger.error("任务被拒绝执行: {}", r.toString());
// 可选择抛出异常或进行其他处理
throw new RejectedExecutionException("任务队列已满");
}
}
}
// 使用自定义拒绝策略
ExecutorService executor = new ThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new LoggingRejectedExecutionHandler(log)
);
参数调优实战步骤
- 初始值设置:
corePoolSize = CPU核心数 × 2
maxPoolSize = corePoolSize × 2
workQueue = new LinkedBlockingQueue<>(1000)
- 压测监控:
- 关注指标:CPU 利用率、线程数波动、队列积压时间
- 工具:JVisualVM、Prometheus + Grafana
- 动态调整:
- 若 CPU 利用率 < 70% 且队列积压,增加
corePoolSize
- 若线程数频繁达到
maxPoolSize
,增加队列容量或maxPoolSize
- 若出现 OOM,减小队列容量和
maxPoolSize
- 若 CPU 利用率 < 70% 且队列积压,增加
最佳实践总结
- CPU 密集型:小核心数 + 小队列 + 大 maxPoolSize
- IO 密集型:大核心数 + 大队列 + 适中 maxPoolSize
- 异常处理:组合使用 Future 捕获 + UncaughtExceptionHandler + 任务包装
- 动态监控:实现线程池参数动态调整接口,对接配置中心
在多线程环境下,ThreadLocal 是一个很重要的工具。请讲讲 ThreadLocal 的原理和应用场景。
ThreadLocal 核心原理
数据隔离机制
ThreadLocal 为每个线程提供独立变量副本,核心实现基于 Thread
类的 threadLocals
属性:
- 每个
Thread
对象包含一个ThreadLocalMap
类型的threadLocals
变量 ThreadLocalMap
是ThreadLocal
的内部类,本质是哈希表,键为ThreadLocal
实例,值为线程副本值- 线程访问变量时,通过
ThreadLocal.get()
从自身的threadLocals
中获取值
关键方法源码解析
// ThreadLocal.get() 核心逻辑
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// ThreadLocalMap 内部结构
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // 强引用存储线程副本值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
弱引用与内存泄漏隐患
ThreadLocalMap.Entry
的键(ThreadLocal
)使用弱引用(WeakReference
)- 若
ThreadLocal
对象被垃圾回收,Entry
的键变为null
,但值(value
)仍被线程强引用 - 若线程长期存活(如线程池中的线程),未被回收的
value
会导致内存泄漏
Web 应用中管理数据库连接示例
基于 ThreadLocal 的连接池封装
public class DatabaseConnectionManager {
// 声明 ThreadLocal 存储数据库连接
private static final ThreadLocal<Connection> CONNECTION_HOLDER = new ThreadLocal<>();
// 数据库连接池(如 HikariCP)
private DataSource dataSource;
public DatabaseConnectionManager(DataSource dataSource) {
this.dataSource = dataSource;
}
// 获取当前线程的数据库连接
public Connection getConnection() throws SQLException {
Connection conn = CONNECTION_HOLDER.get();
if (conn == null) {
conn = dataSource.getConnection();
CONNECTION_HOLDER.set(conn); // 保存到 ThreadLocal
}
return conn;
}
// 关闭连接(需在请求结束时调用)
public void closeConnection() {
Connection conn = CONNECTION_HOLDER.get();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// 异常处理
} finally {
CONNECTION_HOLDER.remove(); // 清除 ThreadLocal 引用
}
}
}
}
在 Web 过滤器中集成
public class DatabaseConnectionFilter implements Filter {
private DatabaseConnectionManager connectionManager;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// 请求开始时获取连接
connectionManager.getConnection();
chain.doFilter(request, response); // 处理业务逻辑
} catch (SQLException e) {
// 数据库异常处理
} finally {
// 请求结束时关闭连接并清除 ThreadLocal
connectionManager.closeConnection();
}
}
}
ThreadLocal 典型应用场景
Web 应用请求上下文
存储当前请求的用户信息、请求参数等
public class RequestContext {
private static final ThreadLocal<Long> USER_ID_HOLDER = new ThreadLocal<>();
public static void setUserId(Long userId) {
USER_ID_HOLDER.set(userId);
}
public static Long getUserId() {
return USER_ID_HOLDER.get();
}
public static void clear() {
USER_ID_HOLDER.remove();
}
}
日志上下文传递
在多线程环境中传递请求日志标识(如 TraceId)
public class LogContext {
private static final ThreadLocal<String> TRACE_ID_HOLDER = new ThreadLocal<>();
public static void setTraceId(String traceId) {
TRACE_ID_HOLDER.set(traceId);
}
public static String getTraceId() {
return TRACE_ID_HOLDER.get();
}
}
事务上下文管理
在分布式事务中传递事务状态
public class TransactionContext {
private static final ThreadLocal<TransactionStatus> TRANSACTION_HOLDER = new ThreadLocal<>();
// 其他事务管理方法...
}
内存泄漏原因与解决方案
内存泄漏产生条件
ThreadLocal
对象被回收(键为null
)- 线程长期存活(如线程池中的线程)
- 未主动调用
ThreadLocal.remove()
解决方案
主动清除引用:在使用完毕后调用 ThreadLocal.remove()
try {
// 使用 ThreadLocal
} finally {
threadLocal.remove(); // 关键步骤
}
使用弱引用 + 定期清理:ThreadLocalMap
内部会在 get/set
时清理键为 null
的 Entry
// ThreadLocalMap.set() 中的清理逻辑
private void set(ThreadLocal<?> key, Object value) {
// ...
cleanSomeSlots(i, sz); // 清理过期 Entry
}
避免线程池线程复用问题:在 Web 应用中,通过过滤器 / 拦截器确保请求结束后清除 ThreadLocal
// Spring 中使用 RequestContextHolder 类似的机制
public class ThreadLocalCleanupInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// 清除所有 ThreadLocal
clearAllThreadLocals();
}
}
ThreadLocal 与其他线程安全方案对比
方案 | 核心机制 | 适用场景 | 性能开销 |
---|---|---|---|
ThreadLocal | 线程本地变量副本 | 读多写少,需线程隔离场景 | 低(无锁竞争) |
synchronized | 互斥锁 | 共享资源互斥访问 | 中(锁竞争) |
ReentrantLock | 可重入锁 | 复杂锁逻辑,需公平性控制 | 高(锁开销) |
Atomic 类 | 原子操作(CAS) | 简单数值计算 | 低(无锁) |
最佳实践总结
- 使用原则
- 每个 ThreadLocal 实例只存储单一变量
- 避免在静态方法中使用非静态 ThreadLocal
- 优先使用
ThreadLocal<T>
泛型声明
- 内存泄漏防范
- 在
finally
块中确保调用remove()
- 结合
try-with-resources
模式管理 ThreadLocal - 对线程池线程,在任务执行前后添加清理逻辑
- 在
- 性能优化
- 复用 ThreadLocal 实例,避免频繁创建
- 对大对象副本,考虑使用对象池减少创建开销
在多线程编程中,Semaphore(信号量)是一种控制并发访问数量的工具。假设你要限制同时访问某个特定资源的线程数量为 5 个,你会如何使用 Semaphore 来实现这一功能呢?在实际项目里,当线程获取不到信号量时,你是怎么处理这种情况的,是让线程等待、直接返回错误,还是采取其他策略?
Semaphore 限流实现与获取失败处理策略
使用 Semaphore 限制并发线程数
核心思路:通过 Semaphore
控制同时访问资源的线程上限,获取许可后执行操作,释放许可时归还资源。
import java.util.concurrent.Semaphore;
public class ResourceAccessLimiter {
private final Semaphore semaphore;
private final Resource resource; // 被保护的资源
public ResourceAccessLimiter(int maxConcurrentThreads, Resource resource) {
// 创建公平信号量,限制最大并发数为5
this.semaphore = new Semaphore(maxConcurrentThreads, true);
this.resource = resource;
}
public void accessResource() throws InterruptedException {
// 尝试获取许可(阻塞直到有可用许可)
semaphore.acquire();
try {
// 执行对共享资源的访问
resource.doSomething();
} finally {
// 释放许可,必须在finally中确保释放
semaphore.release();
}
}
// 尝试非阻塞获取许可
public boolean tryAccessResource() {
// 尝试立即获取许可,成功返回true,失败返回false
if (semaphore.tryAcquire()) {
try {
resource.doSomething();
return true;
} finally {
semaphore.release();
}
}
return false;
}
}
执行流程:
- 初始化
Semaphore(5, true)
创建 5 个许可的公平信号量 - 线程调用
acquire()
阻塞等待许可 - 获取许可后执行资源访问
- 执行完毕在
finally
块中调用release()
释放许可
获取不到信号量的处理策略
- 阻塞等待(默认策略)
public void handleByBlocking() throws InterruptedException {
// 阻塞直到获取许可,可能无限等待
semaphore.acquire();
try {
// 处理资源
} finally {
semaphore.release();
}
}
适用场景:后台任务、无需快速响应的操作
风险:若长时间无可用许可,可能导致线程饥饿
public void handleByTimeout() {
try {
// 等待5秒,超时返回false
if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
try {
// 处理资源
} finally {
semaphore.release();
}
} else {
// 超时处理逻辑
throw new TimeoutException("获取许可超时");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// 中断处理逻辑
}
}
适用场景:用户交互类操作,需要响应超时
public void handleByImmediateFailure() {
if (semaphore.tryAcquire()) {
try {
// 处理资源
} finally {
semaphore.release();
}
} else {
// 直接返回失败或抛出异常
throw new ResourceBusyException("资源繁忙,请稍后重试");
}
}
适用场景:高并发接口限流,快速失败减少等待
public void handleByDegradation() {
if (semaphore.tryAcquire()) {
try {
// 执行正常逻辑
processWithFullFeature();
} finally {
semaphore.release();
}
} else {
// 执行降级逻辑(如返回缓存数据、简化结果)
processWithDegradedFeature();
}
}
适用场景:微服务降级、熔断机制
private final BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>(100);
public void handleByQueueing() {
if (semaphore.tryAcquire()) {
try {
// 有许可,直接执行
processResource();
} finally {
semaphore.release();
}
} else {
// 无许可,放入队列等待
taskQueue.offer(() -> {
try {
semaphore.acquire();
try {
processResource();
} finally {
semaphore.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 启动后台线程处理队列任务(实际项目中建议使用线程池)
new Thread(() -> {
try {
Runnable task = taskQueue.take();
task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
适用场景:异步任务处理,平滑流量峰值
生产环境最佳实践
结合监控告警
// 定期监控信号量使用情况
public void monitorSemaphore() {
int availablePermits = semaphore.availablePermits();
if (availablePermits < 1) {
// 触发告警(如发送邮件、短信)
alertService.sendAlert("资源访问达到上限", availablePermits);
}
}
动态调整许可数
// 根据系统负载动态调整许可数
public void adjustPermitsBasedOnLoad() {
double systemLoad = systemMonitor.getSystemLoad();
if (systemLoad > 0.8) {
// 高负载时减少并发
semaphore.reducePermits(2);
} else if (systemLoad < 0.3) {
// 低负载时增加并发
semaphore.release(2);
}
}
异常处理与资源释放
public void safeAccessResource() {
try {
semaphore.acquire();
try {
// 可能抛出异常的操作
riskyOperation();
} catch (Exception e) {
// 记录异常但仍释放许可
logger.error("资源访问异常", e);
} finally {
semaphore.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
策略选择建议
策略 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
阻塞等待 | 后台批处理、非实时任务 | 实现简单,确保任务执行 | 可能导致线程长时间阻塞 |
超时等待 | 用户交互类操作(如 Web 请求) | 避免无限等待,提升用户体验 | 需要处理超时逻辑 |
立即失败 | 高并发 API 限流、熔断场景 | 快速响应,减轻系统负担 | 可能导致请求丢失 |
降级处理 | 微服务降级、熔断机制 | 保证核心功能,提升可用性 | 需要预实现降级逻辑 |
任务排队 | 流量削峰、异步处理场景 | 平滑流量,避免系统过载 | 增加实现复杂度,需处理队列溢出 |
推荐组合方案:
超时等待 + 降级处理 + 监控告警
即设置合理超时时间,超时后执行降级逻辑,同时监控信号量使用情况触发告警。
CopyOnWriteArrayList 和 ConcurrentHashMap 原理与实践分析
CopyOnWriteArrayList 工作原理
核心机制:写时复制(Copy-On-Write)
- 读操作无锁:直接访问内部数组,无需加锁
- 写操作流程:
- 复制当前数组到新数组
- 在新数组上执行修改操作
- 通过原子引用(volatile)替换旧数组
- 源码关键片段:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁保证复制操作原子性
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制数组
newElements[len] = e;
setArray(newElements); // 原子替换数组
return true;
} finally {
lock.unlock();
}
}
private void setArray(Object[] a) {
array = a; // array 是 volatile 引用
}
迭代器实现
- 迭代器基于创建时的数组快照,不反映后续修改
- 迭代过程中不允许修改,否则抛出
UnsupportedOperationException
- 适用于 “读多写少” 且迭代时不修改的场景
ConcurrentHashMap 工作原理(Java 8 版本)
分段锁进化为细粒度锁
- 数据结构:数组 + 链表 + 红黑树(链表长度 ≥8 时转换)
- 锁机制:
- 读操作:通过
volatile
直接访问,无需加锁 - 写操作:对目标节点加
synchronized
锁
- 读操作:通过
- CAS + 锁结合
// 插入节点核心逻辑
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ...
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS 无锁插入
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
// ...
else {
// 对链表头节点加锁
synchronized (f) {
// ... 链表/红黑树操作 ...
}
}
// ...
}
volatile 保证可见性
- 数组引用
table
和节点value
/next
均为volatile
- 读操作无需加锁即可获取最新数据
适用场景对比
特性 | CopyOnWriteArrayList | ConcurrentHashMap |
---|---|---|
核心优势 | 读操作无锁,适合 “读多写少” 场景 | 读写高并发,写操作细粒度锁 |
写操作开销 | 复制数组,开销大(O (n) 时间复杂度) | 细粒度锁,开销小(平均 O (1)) |
内存占用 | 写操作产生副本,内存占用高 | 正常占用,无额外副本 |
适用场景 | 事件监听器、日志记录、缓存读多写少场景 | 计数器、缓存、高并发 Map 操作 |
不适用场景 | 频繁写操作、实时数据更新、大容量集合 | 读操作极少的场景、需要完全锁定的场景 |
典型使用场景示例
CopyOnWriteArrayList 应用
事件监听系统
public class EventListenerManager {
private final CopyOnWriteArrayList<EventListener> listeners =
new CopyOnWriteArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
}
public void removeListener(EventListener listener) {
listeners.remove(listener);
}
public void fireEvent(Event event) {
// 无锁遍历所有监听器
for (EventListener listener : listeners) {
listener.onEvent(event);
}
}
}
ConcurrentHashMap 应用
实时计数器系统
public class TrafficCounter {
private final ConcurrentHashMap<String, AtomicLong> counterMap =
new ConcurrentHashMap<>();
public void increment(String key) {
counterMap.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet();
}
public long getCount(String key) {
AtomicLong count = counterMap.get(key);
return count != null ? count.get() : 0;
}
}
使用中遇到的问题及解决方案
CopyOnWriteArrayList 内存占用过高
- 原因:频繁写操作导致大量数组副本
- 解决方案:
限制列表容量:
// 自定义固定容量的CopyOnWriteArrayList
public class BoundedCopyOnWriteList<E> extends CopyOnWriteArrayList<E> {
private final int maxSize;
public BoundedCopyOnWriteList(int maxSize) {
this.maxSize = maxSize;
}
@Override
public boolean add(E e) {
if (size() >= maxSize) {
throw new IllegalStateException("List is full");
}
return super.add(e);
}
}
定期清理:
// 定时任务清理旧元素
scheduledExecutor.scheduleAtFixedRate(() -> {
if (list.size() > threshold) {
list.removeIf(element -> isOld(element));
}
}, 0, 1, TimeUnit.HOURS);
CopyOnWriteArrayList 迭代器不反映最新数据
- 原因:迭代器基于快照实现
- 解决方案:
// 需要实时更新的迭代场景,改用加锁遍历
public void processElements() {
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
for (E element : list) {
// 处理元素
}
} finally {
lock.unlock();
}
}
ConcurrentHashMap 扩容时的性能波动
- 原因:Java 8 扩容采用 transfer 方法,单线程迁移节点
- 解决方案:
预设置合理初始容量:
// 预估元素数量为10000,负载因子0.75
new ConcurrentHashMap<>(10000 / 0.75 + 1);
手动触发扩容:
// 预热时手动扩容
public void warmUp() {
for (int i = 0; i < warmUpSize; i++) {
map.put("key-" + i, "value-" + i);
}
}
ConcurrentHashMap 的弱一致性读
- 原因:读操作不加锁,可能读到旧数据
- 解决方案:
// 需要强一致性的场景,加锁读
public V getWithLock(K key) {
Node<K,V>[] tab; Node<K,V> e; int n, i, hash;
hash = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, i = (n - 1) & hash)) != null) {
// 对节点加锁获取强一致性数据
if (e.hash == hash) {
if (e instanceof TreeNode)
return ((TreeNode<K,V>)e).getTreeNode(key);
while (e != null) {
if (e.hash == hash && e.key.equals(key)) {
synchronized (e) { // 加锁读
return e.val;
}
}
e = e.next;
}
}
}
return null;
}
最佳实践总结
- 选择原则:
- 读操作频率远高于写操作 → CopyOnWriteArrayList
- 读写操作均频繁 → ConcurrentHashMap
- 需要键值对映射 → 优先选 ConcurrentHashMap
- 性能优化:
- CopyOnWriteArrayList:限制容量、减少写操作频率
- ConcurrentHashMap:预设置容量、避免极端哈希分布
- 一致性保障:
- 弱一致性场景:直接使用原生 API
- 强一致性场景:手动加锁或使用 Collections.synchronizedMap 包装