
1. 并发编程的典型问题场景在Java并发编程实践中开发者经常会遇到一些反复出现的问题模式。这些问题往往源于对线程安全、内存可见性和执行顺序等基础概念的理解不足。根据我多年处理生产环境并发问题的经验以下这些场景几乎在每个Java项目中都会以不同形式出现竞态条件Race Condition当多个线程对共享数据非原子操作时最终结果取决于线程调度顺序死锁Deadlock两个或多个线程互相持有对方需要的锁导致所有线程永久阻塞活锁Livelock线程不断重试失败的操作消耗CPU但无法取得进展资源饥饿Starvation某些线程长期无法获取所需资源内存可见性问题一个线程的修改对另一个线程不可见提示这些问题在单核CPU时代就已存在但在多核处理器成为主流的今天其出现频率和调试难度都呈指数级增长。2. 竞态条件的深度解析2.1 竞态条件的本质特征竞态条件最经典的例子就是检查后执行Check-Then-Act模式。比如下面这个看似简单的计数器public class UnsafeCounter { private int count; public void increment() { count; // 这不是原子操作 } }count实际上包含三个独立操作读取当前值、增加值、写回新值。当两个线程同时执行时可能两个线程都读取到相同的初始值导致最终结果少加一次。2.2 解决方案对比针对竞态条件Java提供了多种解决方案各有适用场景解决方案原理适用场景性能开销synchronized互斥锁方法或代码块同步较高volatile保证可见性单一变量的原子读写较低AtomicXXXCAS操作计数器等场景中等Lock API显式锁控制需要灵活控制的场景较高在实际项目中我通常遵循这样的选择策略优先考虑无锁方案如AtomicInteger简单同步使用synchronized复杂场景使用Lock APIvolatile仅用于标志位等简单场景3. 死锁问题全解3.1 死锁的四个必要条件死锁的产生必须同时满足以下四个条件互斥条件资源一次只能由一个线程持有占有并等待线程持有资源的同时等待其他资源不可抢占已分配的资源不能被强制拿走循环等待存在一个线程资源的环形等待链3.2 实际案例诊断下面是一个典型的死锁代码public class DeadlockDemo { private final Object lockA new Object(); private final Object lockB new Object(); public void method1() { synchronized (lockA) { synchronized (lockB) { // 操作资源 } } } public void method2() { synchronized (lockB) { synchronized (lockA) { // 操作资源 } } } }当线程1执行method1获取lockA时同时线程2执行method2获取lockB两个线程就会互相等待对方释放锁形成死锁。3.3 死锁预防策略根据我处理死锁问题的经验以下策略非常有效锁顺序化总是以固定顺序获取多个锁如按锁对象的hashCode排序锁超时使用tryLock()设置超时时间死锁检测定期检查线程dump使用JMX等工具监控避免嵌套锁尽量减少锁的嵌套层级注意在分布式系统中死锁问题会更加复杂需要考虑分布式锁的实现机制。4. 并发工具类实战技巧4.1 CountDownLatch使用要点CountDownLatch是控制线程等待的利器但在使用时有几个关键点// 典型用法 CountDownLatch latch new CountDownLatch(N); // 工作线程 void doWork() { try { // 执行任务 } finally { latch.countDown(); // 必须放在finally块 } } // 主线程 latch.await(10, TimeUnit.SECONDS); // 总是设置超时常见陷阱忘记countDown()导致主线程永久等待没有设置await超时时间在countDown前发生异常导致计数不足4.2 ThreadLocal的内存泄漏问题ThreadLocal是实现线程封闭的好方法但使用不当会导致内存泄漏public class UserContext { private static final ThreadLocalUser currentUser new ThreadLocal(); // 使用后必须清理 public static void clear() { currentUser.remove(); } }最佳实践尽量使用static final修饰ThreadLocal实例在线程池环境中必须在finally块中调用remove()考虑使用InheritableThreadLocal时需要特别小心5. 并发集合的性能考量5.1 ConcurrentHashMap分段策略Java8中的ConcurrentHashMap实现发生了重大变化版本实现方式并发度特点Java7分段锁固定锁粒度较粗Java8CASsynchronized动态锁粒度更细实际测试表明在大多数场景下Java8的实现性能更好特别是在读多写少的场景。5.2 CopyOnWriteArrayList的适用场景这个集合类的特点是写操作时复制整个底层数组因此适用场景读多写少遍历操作远多于修改操作数据量不大因为每次修改都要复制不适用场景频繁修改大数据量实时性要求高6. 线程池调优实战6.1 核心参数设置原则线程池配置需要根据具体业务特点调整ThreadPoolExecutor executor new ThreadPoolExecutor( corePoolSize, // CPU密集型CPU核数1 // IO密集型CPU核数*2 maximumPoolSize, // 建议不超过corePoolSize的2倍 keepAliveTime, // 60-120秒 TimeUnit.SECONDS, new LinkedBlockingQueue(queueCapacity), // 需要设置合理大小 new CustomThreadFactory(), // 建议自定义 new CustomRejectedPolicy() // 必须定义拒绝策略 );6.2 常见配置误区队列无限大导致OOM必须设置合理上限无限制的最大线程数同样可能导致资源耗尽使用默认拒绝策略AbortPolicy在生产环境通常不合适忽略线程命名给线程命名有助于问题诊断7. 性能优化技巧7.1 锁粒度优化通过减小锁的粒度可以显著提升并发性能// 不好的实现 - 锁整个方法 public synchronized void processOrder(Order order) { // 长耗时操作 } // 优化后 - 只锁必要部分 public void processOrder(Order order) { synchronized(order) { // 只锁当前订单对象 // 关键操作 } // 其他非关键操作 }7.2 无锁编程技巧在某些场景下可以使用原子变量和CAS操作避免锁// 使用AtomicReference实现无锁栈 public class LockFreeStackT { private AtomicReferenceNodeT top new AtomicReference(); public void push(T item) { NodeT newHead new Node(item); NodeT oldHead; do { oldHead top.get(); newHead.next oldHead; } while (!top.compareAndSet(oldHead, newHead)); } }8. 调试与监控8.1 线程转储分析当遇到死锁或线程阻塞时线程转储是最直接的诊断工具# 获取线程转储 jstack pid thread.dump # 查找死锁 grep -A 10 deadlock thread.dump关键分析点查找BLOCKED状态的线程检查锁持有者和等待者关系注意WAITING状态的线程是否合理8.2 JVM工具推荐VisualVM图形化监控线程状态JConsole查看线程数和死锁Arthas在线诊断工具PrometheusGrafana生产环境监控9. 最佳实践总结根据我在多个高并发项目的经验以下实践最为重要优先使用并发工具类而不是自己造轮子保持简单能用简单方案就不用复杂方案充分测试并发问题往往在特定条件下才会出现代码审查多人检查并发相关代码性能测试用JMH等工具进行基准测试在最近的一个电商项目中我们通过将synchronized替换为ReentrantLock配合合适的锁超时设置成功将订单处理吞吐量提升了40%。关键是要理解每种并发工具的特性和适用场景而不是盲目使用。