1. 为什么我们需要并发编程?
我第一次接触并发编程是在2013年,当时接手了一个电商秒杀系统。在压力测试时,1000个并发用户就让系统崩溃了。那时我才真正理解,单线程处理请求就像只有一个收银台的超市,而并发编程则是开了多个收银通道。
现代计算机普遍采用多核CPU架构,我的开发机就是8核16线程。如果只使用单线程,相当于浪费了87.5%的计算资源。Java作为一门成熟的语言,从1.0版本就内置了线程支持,到现在的Java 21,并发API已经非常完善。
提示:并发(Concurrency)和并行(Parallelism)是不同的概念。并发是逻辑上的同时处理,并行是物理上的同时执行。
2. 并发编程的三大核心问题
2.1 原子性问题:看似简单的i++
去年我在代码审查时发现一个bug:
java复制private int count = 0;
public void add() {
count++; // 这行代码实际上包含3个操作
}
在字节码层面,count++会被编译为:
- 读取count值
- 值+1
- 写回count
在多线程环境下,两个线程可能同时读取到相同的值,导致最终结果不符合预期。这就是典型的原子性问题。
解决方案:
java复制private AtomicInteger count = new AtomicInteger(0);
public void add() {
count.incrementAndGet(); // 原子操作
}
2.2 可见性问题:缓存导致的"幽灵数据"
我曾在生产环境遇到一个诡异的问题:某个配置修改后,部分服务器始终读取旧值。这就是可见性问题 - 线程A修改了变量,线程B却看不到变化。
Java内存模型(JMM)规定:
- 每个线程有自己的工作内存
- 主内存是所有线程共享的
- volatile关键字保证可见性
示例:
java复制private volatile boolean flag = true; // 保证所有线程看到最新值
public void stop() {
flag = false; // 修改立即对其他线程可见
}
2.3 有序性问题:编译器的"优化陷阱"
有一次我调试一个偶现的NPE,最终发现是双重检查锁(DCL)的问题:
java复制public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题出在这里!
}
}
}
return instance;
}
}
问题在于new操作可能被重排序:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
如果2和3被重排序,其他线程可能拿到未初始化的对象。解决方案是使用volatile:
java复制private static volatile Singleton instance;
3. Java并发工具包(JUC)深度解析
3.1 线程池的正确打开方式
我见过太多直接new Thread的代码了,这是非常危险的做法。正确的线程池使用姿势:
java复制// 推荐使用ThreadPoolExecutor而不是Executors
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 任务队列
new ThreadFactoryBuilder().setNameFormat("my-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
注意:阿里巴巴Java开发规范明确禁止使用Executors创建线程池,因为默认队列是无界的,可能导致OOM。
3.2 ConcurrentHashMap的巧妙设计
我在处理一个百万级数据缓存时,对比测试了不同方案:
- HashMap + synchronized:吞吐量 200 ops/s
- Hashtable:吞吐量 500 ops/s
- ConcurrentHashMap:吞吐量 12000 ops/s
ConcurrentHashMap的并发秘诀:
- JDK7采用分段锁(Segment)
- JDK8改为CAS+synchronized
- 读操作完全无锁
示例用法:
java复制ConcurrentMap<String, Object> cache = new ConcurrentHashMap<>();
// 线程安全的putIfAbsent
cache.putIfAbsent("key", new Object());
// 原子性更新
cache.compute("key", (k, v) -> v == null ? new Object() : modify(v));
3.3 AQS:并发工具的核心骨架
AbstractQueuedSynchronizer是JUC的基石,理解它就能明白:
- ReentrantLock如何实现可重入
- CountDownLatch如何计数
- Semaphore如何控制并发数
以ReentrantLock为例,其核心逻辑:
java复制final void lock() {
if (compareAndSetState(0, 1)) // CAS尝试获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 进入队列等待
}
4. 实战中的并发模式
4.1 生产者-消费者模式
我在日志收集系统中实现过这个模式:
java复制BlockingQueue<LogEntry> queue = new LinkedBlockingQueue<>(1000);
// 生产者
public void produce(LogEntry log) {
queue.put(log); // 队列满时自动阻塞
}
// 消费者
public void consume() {
while (running) {
LogEntry log = queue.take(); // 队列空时自动阻塞
process(log);
}
}
4.2 Fork/Join框架
处理一个大型计算任务时,我使用了ForkJoinPool:
java复制class ComputeTask extends RecursiveTask<Long> {
private final int[] array;
private final int start, end;
protected Long compute() {
if (end - start < 1000) { // 小任务直接计算
return computeDirectly();
}
int mid = (start + end) / 2;
ComputeTask left = new ComputeTask(array, start, mid);
ComputeTask right = new ComputeTask(array, mid, end);
left.fork(); // 异步执行子任务
return right.compute() + left.join(); // 合并结果
}
}
4.3 CompletableFuture异步编排
最近我在微服务调用中大量使用:
java复制CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(id));
CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() -> orderService.getOrder(id));
userFuture.thenCombine(orderFuture, (user, order) -> {
// 合并两个异步结果
return new UserOrderVO(user, order);
}).exceptionally(ex -> {
// 统一异常处理
return fallback();
});
5. 并发调试与性能优化
5.1 死锁检测与分析
我常用的诊断命令:
bash复制jstack <pid> # 查看线程栈
jconsole # 图形化监控
典型死锁特征:
code复制Found one Java-level deadlock:
Thread A waiting to lock monitor 1 (held by Thread B)
Thread B waiting to lock monitor 2 (held by Thread A)
5.2 并发性能测试要点
JMeter测试时要注意:
- 合理设置ramp-up时间
- 使用不同的线程组模拟混合场景
- 监控GC情况和线程状态
5.3 常见性能瓶颈
根据我的经验,主要瓶颈点:
- 锁竞争:减少锁粒度,使用读写锁
- 上下文切换:控制线程数量
- 内存屏障:合理使用volatile
- 伪共享:@Contended注解填充
6. Java并发演进与新特性
从Java 5到Java 21,并发API的主要演进:
- Java 5:JUC工具包
- Java 7:Fork/Join框架
- Java 8:CompletableFuture
- Java 9:响应式流
- Java 21:虚拟线程(Virtual Threads)
虚拟线程示例:
java复制try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
} // 自动等待所有任务完成
7. 我的并发编程实践心得
- 优先使用高级并发工具(如ConcurrentHashMap),而不是自己造轮子
- 同步代码块尽量小,锁的粒度尽量细
- 多使用不可变对象(如String、BigInteger)
- 线程池参数要根据实际场景调整
- 生产环境一定要做并发压力测试
最后分享一个真实案例:我们系统曾因为SimpleDateFormat的线程安全问题导致日期错乱。教训是:要么每次new实例,要么使用ThreadLocal。并发编程就是这样,一个小疏忽就可能引发大问题。
