1. 为什么Java并发编程如此重要?
在当今互联网应用中,高并发处理能力已成为系统设计的核心诉求。我曾在一次电商大促中亲眼目睹,由于对并发控制理解不足,一个本该支撑10万QPS的系统在2万并发时就彻底崩溃。事后排查发现,问题根源在于开发人员简单套用了synchronized关键字,却不知道它会导致线程饥饿和死锁。
Java并发编程之所以被称为"艺术",是因为它需要在性能、安全性和可维护性之间找到精妙的平衡点。一个合格的Java开发者必须深入理解:
- 线程生命周期管理(新建、就绪、运行、阻塞、终止)
- 共享资源的安全访问控制
- 内存可见性与指令重排序问题
- 并发工具类的适用场景
警告:很多开发者误以为只要用了
ConcurrentHashMap就万事大吉,实际上错误的使用方式仍然会导致数据不一致。我曾见过有人因为不了解computeIfAbsent的原子性语义,在并发场景下产生了NPE。
2. Java内存模型(JMM)深度解析
2.1 可见性问题与happens-before原则
在传统认知中,代码顺序就是执行顺序。但在多核CPU时代,这个假设完全错误。考虑以下代码:
// 线程A context = loadContext(); // 1 initialized = true; // 2 // 线程B while(!initialized) { Thread.sleep(100); } use(context); // 可能抛出NPE!即使线程A先执行1再执行2,线程B仍可能看到initialized=true但context还未初始化的状态。这就是典型的可见性问题。
JMM通过happens-before规则建立跨线程的操作可见性保证,关键规则包括:
- 程序顺序规则:同一线程内的操作按代码顺序
- 锁规则:解锁操作happens-before后续加锁操作
- volatile规则:写操作happens-before后续读操作
- 线程启动规则:线程A启动线程B,那么A在启动B前的操作对B可见
2.2 volatile的适用场景与误区
volatile常被误解为"轻量级锁",其实它的核心作用是:
- 禁止指令重排序
- 保证可见性
典型应用场景:
- 状态标志位(如shutdown信号)
- 单例模式的双重检查锁定
但要注意:
volatile int count = 0; count++; // 这不是原子操作!我曾用JMH测试发现,在100个线程各执行100万次++操作时,volatile变量的最终值可能只有500万左右。正确做法是使用AtomicInteger。
3. 锁的进阶使用技巧
3.1 synchronized的优化历程
从JDK6开始,synchronized经历了重大优化:
- 偏向锁:单个线程重复获取锁时几乎零开销
- 轻量级锁:通过CAS避免OS层面的线程阻塞
- 重量级锁:真正的互斥锁,会引发线程上下文切换
通过-XX:+PrintFlagsFinal可以看到默认开启锁升级。但在高竞争场景下,建议直接用ReentrantLock。
3.2 ReentrantLock的实战技巧
相比synchronized,ReentrantLock提供了更多控制:
Lock lock = new ReentrantLock(true); // 公平锁 try { if(lock.tryLock(100, TimeUnit.MILLISECONDS)) { // 业务逻辑 } } finally { lock.unlock(); // 必须手动释放! }我在支付系统超时订单处理中,使用tryLock实现了:
- 等待超时自动放弃
- 可中断的锁获取
- 按申请顺序获取锁(公平性)
3.3 读写锁的性能优化
ReentrantReadWriteLock在读多写少场景下能大幅提升吞吐量。一个常见的误区是:
// 错误用法:读锁内执行写操作 readLock.lock(); try { if(cache.isEmpty()) { writeLock.lock(); // 死锁风险! // ... } } finally {...}正确的做法是先释放读锁再获取写锁,或者使用StampedLock的乐观读:
StampedLock sl = new StampedLock(); long stamp = sl.tryOptimisticRead(); // 读操作 if(!sl.validate(stamp)) { stamp = sl.readLock(); try { // 重新读 } finally { sl.unlockRead(stamp); } }4. 并发容器选型指南
4.1 ConcurrentHashMap的演进
JDK8对CHM进行了重大改进:
- 取消分段锁,改用CAS+synchronized
- 链表长度超过8时转为红黑树
- 提供了丰富的原子操作方法
一个实用技巧:
map.compute(key, (k, v) -> { if(v == null) return initValue; return v.update(); });但要注意:compute方法内的逻辑应该尽量简单,避免持有锁时间过长。我曾见过有人在compute中调用RPC导致整个Map性能骤降。
4.2 阻塞队列的四种拒绝策略
当线程池队列满时,处理策略包括:
- AbortPolicy(默认):抛出RejectedExecutionException
- CallerRunsPolicy:由提交任务的线程执行
- DiscardPolicy:静默丢弃
- DiscardOldestPolicy:丢弃队列最老任务
在订单系统中,我们采用自定义策略:
new ThreadPoolExecutor(..., new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if(!e.isShutdown()) { try { e.getQueue().offer(r, 100, TimeUnit.MILLISECONDS); } catch(InterruptedException ie) { Thread.currentThread().interrupt(); } } } });5. 线程池的实战陷阱
5.1 参数配置的黄金法则
根据任务类型选择线程池参数:
- CPU密集型:核心线程数 = CPU核数 + 1
- IO密集型:核心线程数 = CPU核数 * (1 + 平均等待时间/平均计算时间)
一个真实的性能优化案例:
// 原配置(导致CPU 100%) ExecutorService es = Executors.newFixedThreadPool(200); // 优化后 ThreadPoolExecutor tpe = new ThreadPoolExecutor( 50, 100, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), new NamedThreadFactory("order-process"), new CustomRejectPolicy());5.2 线程泄漏检测方案
通过继承ThreadPoolExecutor实现监控:
protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); if(t != null) { monitor.logError("Task failed", t); } }还可以通过JMX暴露关键指标:
ManagementFactory.getPlatformMBeanServer().registerMBean( new ThreadPoolMonitor(executor), new ObjectName("com.xxx:type=ThreadPool,name=orderService"));6. 异步编程新范式
6.1 CompletableFuture的组合魔法
相比Future,CompletableFuture提供了强大的组合能力:
CompletableFuture.supplyAsync(this::queryOrder, ioPool) .thenApplyAsync(this::processPayment, cpuPool) .thenCombine( queryInventoryAsync(), (payment, inventory) -> checkStock(payment, inventory)) .exceptionally(ex -> { log.error("Process failed", ex); return fallbackResult; });我在风控系统中使用这种模式,将串行5秒的操作优化到1秒内完成。
6.2 虚拟线程的使用限制
JDK19引入的虚拟线程虽好,但要注意:
- 不适合计算密集型任务
- synchronized块会pin住载体线程
- 原生代码调用会阻塞线程
最佳实践:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000) .forEach(i -> executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; })); } // 自动等待所有任务完成7. 并发调试技巧
7.1 死锁检测三件套
- jstack:直接查看线程堆栈
jstack -l <pid> > thread_dump.txt - JConsole:可视化查看锁持有情况
- Arthas:动态监控锁竞争
watch java.util.concurrent.locks.ReentrantLock getQueueLength
7.2 并发测试工具
使用JCStress测试并发正确性:
@JCStressTest @Outcome(id = "1, 1", expect = Expect.ACCEPTABLE) @State public class MyConcurrentTest { private int x; @Actor public void thread1(II_Result r) { x = 1; r.r1 = x; } @Actor public void thread2(II_Result r) { x = 2; r.r2 = x; } }8. 性能优化实战案例
在最近的消息推送系统优化中,我们通过以下步骤将吞吐量从5k QPS提升到50k:
- 将
synchronized改为StampedLock(提升30%) - 用
LongAdder替代AtomicLong计数(减少CAS竞争) - 引入线程本地缓存减少共享访问
- 使用
ForkJoinPool处理批量任务
关键指标监控显示,CPU利用率从90%降至60%,GC时间减少70%。这个案例充分证明,合理的并发控制能带来质的飞跃。