1. JUC并发编程概述
Java并发编程是每个Java开发者必须掌握的核心技能之一。JUC(java.util.concurrent)包自Java 5引入以来,已经成为处理高并发场景的标准工具集。这个包提供了一系列比基础线程操作更强大、更灵活的并发控制组件。
在实际项目中,我经常遇到这样的场景:一个电商平台的秒杀系统需要在1秒内处理上万次库存扣减请求。如果使用传统的synchronized关键字,性能会急剧下降,而采用JUC中的原子类和并发容器,系统吞吐量可以提升5-10倍。这正是JUC的价值所在——它让Java开发者能够构建高性能、线程安全的应用程序。
JUC包主要包含以下核心组件:
- 原子变量类(AtomicInteger等)
- 并发容器(ConcurrentHashMap等)
- 锁机制(ReentrantLock等)
- 线程池框架(ThreadPoolExecutor等)
- 同步工具类(CountDownLatch等)
2. 原子操作与CAS原理
2.1 原子类实战应用
AtomicInteger是JUC中最常用的原子类之一。在最近的一个订单编号生成器项目中,我使用AtomicInteger实现了高效的序列号生成:
java复制public class OrderIdGenerator {
private static final AtomicInteger counter = new AtomicInteger(0);
public static String generate() {
int id = counter.incrementAndGet();
return "ORD-" + LocalDate.now() + "-" + String.format("%05d", id);
}
}
这种实现相比synchronized方法,性能测试显示TPS(每秒事务数)提升了近8倍。关键在于它使用了CAS(Compare-And-Swap)机制,避免了传统锁带来的上下文切换开销。
2.2 CAS的底层实现
CAS操作包含三个操作数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
当且仅当V的值等于A时,处理器才会用B更新V的值。在Java中,这通过Unsafe类的native方法实现。但开发者通常不需要直接使用Unsafe,原子类已经提供了安全的封装。
注意:虽然CAS高效,但在高竞争环境下可能导致大量自旋,此时考虑使用LongAdder替代AtomicLong能获得更好的性能。
3. 并发容器深度解析
3.1 ConcurrentHashMap设计精妙
ConcurrentHashMap是面试中最常被问及的并发容器。在开发一个实时风控系统时,我对其进行了深度优化:
java复制ConcurrentHashMap<String, UserBehavior> behaviorMap = new ConcurrentHashMap<>(1000, 0.75f, 32);
关键参数说明:
- 初始容量1000:避免频繁扩容
- 并发级别32:将数据分成32段,减少锁竞争
JDK8后的实现抛弃了分段锁,改用:
- Node数组+链表/红黑树
- synchronized+CAS实现锁细化
- 扩容时多线程协同迁移
3.2 阻塞队列应用场景
ArrayBlockingQueue在我实现的日志收集系统中发挥了重要作用:
java复制BlockingQueue<LogEntry> queue = new ArrayBlockingQueue<>(1000);
// 生产者
executor.submit(() -> {
while(true) {
LogEntry log = getLog();
queue.put(log); // 队列满时自动阻塞
}
});
// 消费者
executor.submit(() -> {
while(true) {
LogEntry log = queue.take(); // 队列空时自动阻塞
processLog(log);
}
});
这种生产者-消费者模式完美解决了突发流量导致的系统过载问题。
4. 锁机制进阶使用
4.1 ReentrantLock vs synchronized
在开发交易系统时,我针对锁选择做了详细对比:
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 锁获取方式 | 显式调用lock()/unlock() | 自动获取释放 |
| 尝试非阻塞获取 | tryLock()支持 | 不支持 |
| 公平锁 | 可配置 | 非公平 |
| 条件变量 | 支持多个Condition | 单一wait/notify |
| 性能 | JDK6后基本持平 | JDK6后优化明显 |
4.2 读写锁优化实践
在一个配置中心项目中,我使用ReadWriteLock实现了多读单写的场景:
java复制class ConfigCache {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private Map<String, String> cache = new HashMap<>();
public String getConfig(String key) {
lock.readLock().lock();
try {
return cache.get(key);
} finally {
lock.readLock().unlock();
}
}
public void updateConfig(String key, String value) {
lock.writeLock().lock();
try {
cache.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
}
实测显示,读多写少场景下吞吐量提升15倍以上。
5. 线程池最佳实践
5.1 参数配置黄金法则
在搭建订单处理系统时,我总结出线程池配置经验:
java复制ThreadPoolExecutor executor = new ThreadPoolExecutor(
核心线程数 = CPU核心数 * 2,
最大线程数 = CPU核心数 * 4,
保持时间 = 30秒,
工作队列 = new ArrayBlockingQueue<>(1000),
拒绝策略 = new CallerRunsPolicy()
);
关键点:
- IO密集型任务:线程数可以较多(2N~4N)
- CPU密集型任务:线程数接近核心数(N+1)
- 队列容量需要根据业务特点调整
- 拒绝策略推荐CallerRunsPolicy(由调用线程直接运行)
5.2 异常处理陷阱
新手常犯的错误是忽略任务异常处理:
java复制executor.submit(() -> {
try {
riskyOperation();
} catch (Exception e) {
log.error("Task failed", e); // 必须捕获,否则异常会被吞掉
}
});
更优雅的做法是实现Thread.UncaughtExceptionHandler:
java复制ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Thread {} failed", thread.getName(), throwable);
});
return t;
};
6. 同步工具类实战
6.1 CountDownLatch应用
在性能测试框架中,我用CountDownLatch实现并发启动:
java复制CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executor.execute(() -> {
try {
startLatch.await(); // 所有线程在此等待
doTest();
} finally {
endLatch.countDown();
}
});
}
long start = System.nanoTime();
startLatch.countDown(); // 统一释放所有线程
endLatch.await(); // 等待所有线程完成
long duration = System.nanoTime() - start;
6.2 CyclicBarrier vs CountDownLatch
两者区别常被混淆:
| 特性 | CyclicBarrier | CountDownLatch |
|---|---|---|
| 重置 | 自动重置 | 不可重置 |
| 计数方向 | 递增到阈值 | 递减到零 |
| 等待动作 | 所有线程互相等待 | 线程等待计数归零 |
| 典型用途 | 多阶段并行计算 | 启动/结束同步 |
7. 并发编程避坑指南
7.1 死锁预防策略
在支付系统开发中,我总结出死锁预防四原则:
- 固定锁获取顺序(如按ID升序)
- 使用tryLock()设置超时
- 避免嵌套锁
- 使用jstack定期检测
示例安全代码:
java复制public void transfer(Account from, Account to, int amount) {
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;
synchronized (first) {
synchronized (second) {
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
}
}
}
}
7.2 可见性问题排查
内存可见性导致的bug最难排查。在开发缓存系统时,我遇到一个典型案例:
java复制class Cache {
private boolean initialized = false; // 错误:缺少volatile
private Map<String, String> data;
public void init() {
data = loadData(); // 耗时操作
initialized = true; // 可能对其他线程不可见
}
public String get(String key) {
if (!initialized) {
init(); // 多线程下可能重复初始化
}
return data.get(key);
}
}
解决方案:
- 对initialized添加volatile修饰
- 或者使用AtomicBoolean
- 最佳实践是直接使用final字段
8. Java内存模型深入
8.1 happens-before规则
理解这些规则对编写正确并发程序至关重要:
- 程序顺序规则:线程内操作按程序顺序
- 锁规则:解锁先于后续加锁
- volatile规则:写先于后续读
- 线程启动规则:start()先于线程内操作
- 传递性:A先于B,B先于C,则A先于C
8.2 双重检查锁定模式
单例模式的经典实现需要特别注意:
java复制class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
volatile关键字在这里防止指令重排序导致的未初始化对象被引用。在Java 5之前,这种实现仍然是线程不安全的。
