1. 项目概述为什么JUC是Java高并发开发的基石如果你正在用Java做后端开发或者准备面试那么“JUC”这个词你一定不陌生。它频繁出现在各种面试八股文里也是实际项目中处理高并发、高性能场景绕不开的核心。但很多朋友对JUC的理解可能还停留在“知道有这么个包里面有几个类”的层面真正用起来总觉得隔着一层纱遇到复杂的并发问题还是容易抓瞎。我自己在带团队和做项目的过程中发现能把JUC这套工具用得得心应手是区分普通开发者和资深开发者的一个重要标志。这篇文章我就想结合自己踩过的坑和实战经验带你快速且彻底地搞懂JUC。我们的目标不是死记硬背API而是建立起一套处理并发问题的“肌肉记忆”和“条件反射”让你看到场景就能想到最合适的工具。JUC全称java.util.concurrent是Java 5.0引入的一个专门用于并发编程的工具包。你可以把它理解为Java内置的一套“并发工具箱”。在它出现之前我们处理多线程主要靠synchronized关键字和基础的wait()、notify()机制。这些机制不是不好但就像用螺丝刀拧所有螺丝有些场景下显得笨重且效率不高。JUC提供了一系列更精细、更高效、功能更强大的工具比如各种锁ReentrantLock, ReadWriteLock、线程安全的集合ConcurrentHashMap, CopyOnWriteArrayList、线程池ThreadPoolExecutor、以及用于协调线程的同步器CountDownLatch, CyclicBarrier, Semaphore等。彻底搞懂JUC意味着你能根据不同的业务场景比如缓存更新、订单处理、批量任务执行精准地选用最合适的并发组件写出既安全线程安全又高效高吞吐、低延迟的代码。2. JUC核心组件深度解析与设计思想要彻底搞懂JUC不能只停留在会用的层面必须理解其背后的设计思想和每个核心组件的适用场景。JUC的设计哲学可以概括为“分离关注点”和“提供更细粒度的控制”。它将同步、互斥、通信等概念抽象成一个个独立的组件让我们可以像搭积木一样构建复杂的并发程序。2.1 锁机制从synchronized到Lock的进化synchronized是Java原生的互斥同步关键字它简单易用JVM会负责加锁和释放锁。但在复杂的场景下它暴露出几个局限性1等待锁的线程无法被中断2尝试获取锁时如果失败会一直阻塞无法设置超时3锁的获取和释放必须在一个代码块或方法内不够灵活。JUC中的Lock接口及其实现类主要是ReentrantLock就是为了解决这些问题而生的。ReentrantLock提供了synchronized不具备的三大能力可中断的锁获取通过lockInterruptibly()方法在等待锁的过程中线程可以响应中断信号这有助于避免死锁或实现更优雅的线程终止逻辑。可超时的锁获取通过tryLock(long time, TimeUnit unit)方法可以指定一个等待时间。如果在规定时间内没拿到锁线程不会无限期阻塞而是返回false程序可以执行其他备选逻辑。公平锁与非公平锁ReentrantLock的构造函数可以传入一个boolean参数指定是创建公平锁还是非公平锁。公平锁保证等待时间最长的线程优先获取锁避免了“饥饿”现象但会带来更多的线程切换开销吞吐量通常较低。非公平锁则允许“插队”吞吐量高是默认且更常用的选择。实操心得在绝大多数高并发场景下请直接使用非公平锁。公平锁带来的额外性能开销在竞争激烈时非常明显。只有在你非常确定需要绝对公平性且性能不是首要考量时才考虑公平锁。除了基本的互斥锁ReadWriteLock读写锁是另一个极其重要的锁工具。它的设计思想是“读读不互斥读写互斥写写互斥”。这对于“读多写少”的场景比如缓存是巨大的性能优化。多个线程可以同时读数据只有写操作才会独占锁。JUC提供了ReentrantReadWriteLock作为其实现。2.2 原子类无锁并发编程的利器在多线程环境下即便是i这样的操作也不是原子的因为它涉及读取、计算、写入三个步骤。传统的做法是给整个方法或代码块加锁但这在竞争激烈时性能损耗大。JUC的原子类如AtomicInteger,AtomicLong,AtomicReference基于CASCompare-And-Swap操作实现。CAS是一种乐观锁机制它包含三个操作数内存位置V、预期原值A和新值B。当且仅当V的值等于A时处理器才会用B更新V的值否则不执行更新。整个操作是一个原子指令通常由CPU硬件层面保证。// 传统方式 private int count 0; public synchronized void increment() { count; } // 使用AtomicInteger private AtomicInteger count new AtomicInteger(0); public void increment() { count.incrementAndGet(); // 底层使用CAS无需显式加锁 }原子类的优势在于它在低至中度竞争环境下性能远高于锁。因为它避免了线程挂起和上下文切换的开销。但是在高竞争环境下CAS可能因为反复失败而进入“自旋”状态消耗CPU资源。这时可能需要结合其他同步策略。2.3 并发容器线程安全的数据结构直接在多线程环境下使用ArrayList、HashMap等非线程安全容器是灾难性的。虽然可以用Collections.synchronizedList包装但这种粗粒度的同步方式性能很差。JUC提供了一系列高性能的并发容器ConcurrentHashMap这是最重要的并发容器。在JDK 1.7及之前它采用“分段锁”机制将数据分成一段一段的每段配一把锁这样不同段的操作可以并发。在JDK 1.8及之后它做了巨大优化大量使用synchronized和CAS来锁住单个链表头或红黑树根节点并发粒度更细性能更高。它是替代Hashtable和同步包装的HashMap的首选。CopyOnWriteArrayList和CopyOnWriteArraySet采用“写时复制”策略。任何修改操作add, set, remove都会在底层创建一个新的数组副本在新副本上操作完成后再将引用指向新数组。读操作则完全无锁。这非常适合“读多写极少”的场景比如监听器列表。需要注意的是写操作开销大且会占用双倍内存不适合频繁修改的场景。ConcurrentLinkedQueue一个基于链接节点的无界线程安全队列。它采用“非阻塞”算法CAS提供了高效的并发入队和出队操作。2.4 线程池管理线程的生命周期直接new Thread()创建线程然后start()在需要处理大量短期异步任务的场景下如Web服务器处理请求会导致频繁创建和销毁线程消耗大量系统资源。线程池的核心思想是“池化技术”预先创建好一些线程放在“池”里来任务时从池中取线程执行执行完毕后再放回池中从而避免频繁创建销毁的开销。JUC的线程池核心是ThreadPoolExecutor类。理解它的构造参数是正确使用的关键public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)corePoolSize核心线程数。即使线程空闲也会保留在池中的线程数量除非设置了allowCoreThreadTimeOut。maximumPoolSize最大线程数。池中允许存在的最大线程数。keepAliveTime空闲线程存活时间。当线程数超过核心线程数时多余的空闲线程在等待新任务时的最长存活时间。workQueue工作队列。用于存放等待执行的任务。常用的有LinkedBlockingQueue无界队列、ArrayBlockingQueue有界队列、SynchronousQueue直接交接队列。handler拒绝策略。当线程池和队列都满了如何处理新提交的任务。内置策略有AbortPolicy直接抛出异常、CallerRunsPolicy由调用者线程执行、DiscardPolicy直接丢弃、DiscardOldestPolicy丢弃队列中最老的任务。注意事项使用Executors工厂类如newFixedThreadPool,newCachedThreadPool创建线程池虽然方便但隐藏了参数细节容易导致问题如newFixedThreadPool使用无界队列可能堆积大量任务导致OOM。生产环境建议直接使用ThreadPoolExecutor构造函数根据业务特点明确配置各个参数。2.5 同步器线程间的协调工具有时候我们需要的不是互斥访问而是让多个线程在某个点上“汇合”或按某种顺序执行。JUC提供了几个强大的同步器CountDownLatch倒计时闩锁一个线程或多个等待其他一组线程完成操作。构造时指定一个计数N。其他线程完成任务后调用countDown()计数减1。等待的线程调用await()会阻塞直到计数减为0。它是一次性的计数归零后无法重置。典型场景主线程等待所有子线程初始化完成后再执行。CyclicBarrier循环栅栏让一组线程互相等待直到所有线程都到达一个公共屏障点然后屏障打开所有线程继续执行并且屏障可以重置复用。构造时指定参与线程数N。每个线程调用await()表示到达屏障并阻塞。当第N个线程调用await()后所有被阻塞的线程被唤醒继续执行。它适用于多轮迭代计算的场景。Semaphore信号量用来控制同时访问特定资源的线程数量。它维护了一组“许可证”。线程执行前需要调用acquire()获取许可证如果无证可用则阻塞执行完后调用release()归还许可证。常用于流量控制如数据库连接池限流。Exchanger交换器用于两个线程之间交换数据。每个线程在调用exchange()方法时会阻塞直到另一个线程也调用此方法然后双方交换数据并继续执行。3. 核心场景实战与避坑指南理论懂了还得在实战中锤炼。下面我们通过几个典型场景看看如何组合运用JUC工具。3.1 场景一构建一个高性能的本地缓存假设我们要实现一个商品信息的本地缓存要求是读非常频繁写相对较少比如商品信息每天只更新一两次。错误做法使用HashMap并用synchronized包装所有方法。这会导致所有读操作也串行化性能极差。正确做法使用ReadWriteLock或直接使用ConcurrentHashMap。方案AReadWriteLock适合缓存逻辑比较复杂需要手动控制读写锁的范围。public class ProductCacheWithRWLock { private final MapString, Product cache new HashMap(); private final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); public Product getProduct(String id) { rwLock.readLock().lock(); try { return cache.get(id); } finally { rwLock.readLock().unlock(); } } public void updateProduct(String id, Product product) { rwLock.writeLock().lock(); try { cache.put(id, product); } finally { rwLock.writeLock().unlock(); } } }方案BConcurrentHashMap更简单直接get操作完全无锁put操作有细粒度锁性能极高是首选。public class ProductCacheWithCHM { private final ConcurrentHashMapString, Product cache new ConcurrentHashMap(); public Product getProduct(String id) { return cache.get(id); // 无锁 } public void updateProduct(String id, Product product) { cache.put(id, product); // 细粒度锁 } }避坑点这里有一个经典的“缓存穿透”问题。如果查询一个不存在的商品IDget会返回null。高并发下大量请求同时查询同一个不存在的ID会导致请求都穿透缓存打到数据库。解决方法可以是缓存空对象null或特殊标记或者使用布隆过滤器Bloom Filter预先过滤。3.2 场景二批量任务执行与结果汇总主线程需要发起100个并行任务比如调用100个外部接口等所有任务执行完毕后汇总结果进行下一步处理。实现使用CountDownLatch或CompletableFutureJDK8。使用CountDownLatchpublic class BatchTaskWithLatch { public void executeBatchTasks() throws InterruptedException { int taskCount 100; CountDownLatch latch new CountDownLatch(taskCount); ExecutorService executor Executors.newFixedThreadPool(10); ListFutureResult futures new ArrayList(); for (int i 0; i taskCount; i) { FutureResult future executor.submit(() - { try { // 执行单个任务 return doTask(); } finally { latch.countDown(); // 任务完成计数减1 } }); futures.add(future); } latch.await(); // 主线程等待所有任务完成 System.out.println(所有任务已完成开始汇总...); // 汇总Future结果 ListResult results new ArrayList(); for (FutureResult future : futures) { results.add(future.get()); } // ... 处理汇总结果 executor.shutdown(); } }使用CompletableFuture更现代功能更强public class BatchTaskWithCF { public void executeBatchTasks() { ListCompletableFutureResult futures new ArrayList(); for (int i 0; i 100; i) { futures.add(CompletableFuture.supplyAsync(() - doTask())); } // 等待所有Future完成 CompletableFutureVoid allFutures CompletableFuture.allOf( futures.toArray(new CompletableFuture[0]) ); // 所有任务完成后获取结果 CompletableFutureListResult resultFuture allFutures.thenApply(v - futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()) ); ListResult results resultFuture.join(); // 阻塞直到获取结果 // ... 处理汇总结果 } }避坑点使用CountDownLatch时务必在finally块中调用countDown()确保任务无论成功失败计数器都能递减避免主线程永远等待。使用线程池时任务抛出的异常会被封装在Future里调用future.get()时才会抛出需要注意异常处理。3.3 场景三实现一个简单的连接池模拟一个数据库连接池最大连接数为10。当线程需要连接时从池中获取如果池已空则等待使用完毕后归还连接。实现使用Semaphore作为许可证控制器配合一个线程安全的队列存放连接对象。public class SimpleConnectionPool { private final int poolSize 10; // 使用信号量控制并发获取连接的数量 private final Semaphore available new Semaphore(poolSize, true); // 使用阻塞队列存放空闲连接 private final BlockingQueueConnection pool new ArrayBlockingQueue(poolSize); public SimpleConnectionPool() { for (int i 0; i poolSize; i) { pool.offer(createConnection()); } } public Connection getConnection() throws InterruptedException { available.acquire(); // 获取许可证如果没有则阻塞 Connection conn pool.poll(); // 从池中取出一个连接 return conn; } public void releaseConnection(Connection conn) { if (conn ! null) { pool.offer(conn); // 归还连接到池中 available.release(); // 释放许可证 } } private Connection createConnection() { // 模拟创建连接 return new MockConnection(); } }避坑点这是一个简化模型。真实的连接池如HikariCP要考虑更多连接有效性检测心跳、空闲连接超时回收、获取连接超时控制、连接泄漏检测等。Semaphore在这里只是控制了“入口”的并发数连接对象本身的管理还需要依赖队列和其他机制。4. 高级主题与性能调优考量当你掌握了基础组件的使用后就需要关注一些更深入的话题这些往往在线上问题排查和性能优化时至关重要。4.1 锁的性能与优化锁是性能的敌人但又是保证正确性的必要手段。优化锁的宗旨是减少锁的粒度、缩短锁的持有时间、降低锁的竞争频率。锁粗化 vs 锁细化如果一段代码中频繁地对同一个锁进行“加锁-释放”操作比如在循环内JVM可能会自动进行“锁粗化”将多次锁合并为一次以减少开销。相反我们编码时应尽量“锁细化”只锁住必要的共享数据部分而不是锁住整个方法或大段代码。锁消除JVM的即时编译器JIT会进行逃逸分析。如果发现某个锁对象不可能被其他线程访问到即不会发生共享就会将这个锁消除。这提示我们应尽量限制变量的作用域。使用无锁数据结构在可能的情况下优先考虑使用原子类AtomicInteger或ConcurrentHashMap这类基于CAS的无锁或细粒度锁结构它们在高并发读场景下优势明显。避免热点竞争像AtomicLong这样的原子类在高并发更新时所有线程都在竞争同一个变量地址CAS失败率会很高。JDK 8提供了LongAdder和DoubleAdder来解决这个问题。它们内部将一个变量拆分成多个Cell线程更新时分散到不同的Cell上最后再汇总极大地减少了竞争在超高并发写场景下性能远超AtomicLong。4.2 线程池的合理配置与监控线程池配置不当是线上服务不稳定的常见原因。这里有几个经验法则任务类型决定池类型CPU密集型计算、处理线程数建议设置为CPU核心数 1。过多线程会导致频繁的上下文切换反而降低性能。I/O密集型网络请求、数据库操作线程数可以设置得多一些因为线程大部分时间在等待I/O。经验公式线程数 CPU核心数 * (1 平均等待时间 / 平均计算时间)。这个比例通常称为阻塞系数在2~10之间都是常见的。例如一个主要调用外部API的服务可以设置为核心数 * 4或更多。队列选择LinkedBlockingQueue无界如果任务提交速度偶尔超过处理速度队列可以起到缓冲作用。但风险是如果任务产生速度持续高于处理速度队列会无限增长最终导致OOM。慎用。ArrayBlockingQueue有界可以防止资源耗尽。当队列满时会根据拒绝策略处理。需要合理评估队列容量。SynchronousQueue不存储元素提交的任务直接交给线程执行如果没有空闲线程则创建新线程未达最大线程数时或执行拒绝策略。这要求线程池有足够的增长能力适合任务量不大但要求低延迟的场景。监控与动态调整Spring Boot的Actuator可以暴露线程池指标。更高级的做法是使用像Hystrix已停更或Resilience4j的线程池隔离或者使用可以动态调整参数的线程池如美团开源的DynamicTp。4.3 常见并发问题与排查技巧即使使用了JUC如果使用不当依然会产生问题。下面是一个快速排查表问题现象可能原因排查思路与解决方案CPU占用率过高1. 死循环2. 大量线程处于可运行状态Runnable激烈竞争CPU3. 大量自旋如CAS失败频繁。1. 使用jstack或Arthas查看线程栈找到占用CPU高的线程。2. 检查是否有不当的循环或锁竞争。3. 对于原子类考虑是否竞争过于激烈可换用LongAdder。响应缓慢吞吐量低1. 锁竞争激烈大量线程阻塞BLOCKED。2. I/O等待。3. 线程池配置不合理如核心线程数过少队列过长。1.jstack查看线程状态统计BLOCKED线程数。2. 使用jstat或可视化工具查看GC情况排除Full GC。3. 检查线程池监控指标调整核心/最大线程数和队列类型。内存溢出OOM1. 使用无界队列如LinkedBlockingQueue的线程池任务堆积。2.ThreadLocal使用后未清理导致内存泄漏尤其在线程池中线程是复用的。1. 检查线程池队列大小。2. 使用内存分析工具如MAT分析堆转储查看占据内存最大的对象。3. 确保ThreadLocal使用后调用remove()。数据不一致1. 存在“先检查后执行”的非原子复合操作Check-Then-Act。2. 误以为单个操作是原子的如ConcurrentHashMap的putIfAbsent是原子的但get后再put不是。1. 使用锁或原子操作如ConcurrentHashMap的computeIfAbsent将复合操作保护起来。2. 仔细阅读API文档确认操作的原子性边界。死锁多个线程互相持有对方需要的锁并等待对方释放。1.jstack可以检测到死锁并报告。2. 编码时遵循固定的锁获取顺序。3. 使用tryLock设置超时超时后释放已持有的锁并重试或记录日志告警。排查工具推荐命令行jps,jstack,jstat,jmap。这是最基础也是最强大的工具。可视化/APMArthas阿里开源功能强大JConsole, VisualVM, Prometheus Grafana监控指标。压测JMeter,iperf网络打流但可类比理解系统压力测试。5. 从JUC到更高阶的并发模型彻底搞懂JUC是构建稳健高并发应用的坚实基础。但技术总是在演进。在现代Java开发中特别是响应式编程和异步处理领域有一些建立在JUC之上或与之互补的技术值得关注CompletableFuture(JDK 8): 它不仅仅是Future的增强版更代表了一种异步编程的范式。它允许你以声明式的、链式调用的方式组合多个异步任务处理它们的完成、异常和结果转换大大简化了复杂的异步流程编排代码。它内部同样使用了ForkJoinPool等线程池。响应式编程 (Reactor, RxJava): 在处理大量并发I/O操作如微服务间调用、消息推送时传统的阻塞式线程模型一个请求一个线程会消耗大量线程资源。响应式编程基于事件驱动和异步非阻塞用更少的资源甚至单线程处理更高的并发。Spring WebFlux就是基于Reactor实现的。它的底层也依赖于JUC中的调度器Schedulers。协程 (Project Loom - 预览特性): 这是Java未来可能引入的“轻量级线程”虚拟线程。它由JVM管理创建和切换开销极低可以创建数十万甚至数百万个而不会导致系统资源耗尽。这有望从根本上简化高并发编程模型让我们可以用接近同步编程的写法一个请求一个虚拟线程获得异步非阻塞的性能。虽然还未正式发布但值得保持关注。我个人在实际项目中的体会是JUC是并发世界的“普通话”必须流利掌握。而CompletableFuture和响应式编程更像是“方言”或“专业术语”在特定的场景如全链路异步、流处理下能发挥巨大威力。我的建议是先扎实练好JUC这套基本功理解线程、锁、同步的本质。当你能熟练运用ExecutorService、ConcurrentHashMap、CountDownLatch等工具解决日常并发问题后再去学习CompletableFuture的链式调用和响应式编程的背压、流控制等概念就会水到渠成发现它们不过是更高层次的抽象和封装。最终你会形成自己的并发工具箱面对不同的业务场景能够自信地选出最趁手的那把“武器”。
Java高并发开发核心:JUC工具包深度解析与实战指南
1. 项目概述为什么JUC是Java高并发开发的基石如果你正在用Java做后端开发或者准备面试那么“JUC”这个词你一定不陌生。它频繁出现在各种面试八股文里也是实际项目中处理高并发、高性能场景绕不开的核心。但很多朋友对JUC的理解可能还停留在“知道有这么个包里面有几个类”的层面真正用起来总觉得隔着一层纱遇到复杂的并发问题还是容易抓瞎。我自己在带团队和做项目的过程中发现能把JUC这套工具用得得心应手是区分普通开发者和资深开发者的一个重要标志。这篇文章我就想结合自己踩过的坑和实战经验带你快速且彻底地搞懂JUC。我们的目标不是死记硬背API而是建立起一套处理并发问题的“肌肉记忆”和“条件反射”让你看到场景就能想到最合适的工具。JUC全称java.util.concurrent是Java 5.0引入的一个专门用于并发编程的工具包。你可以把它理解为Java内置的一套“并发工具箱”。在它出现之前我们处理多线程主要靠synchronized关键字和基础的wait()、notify()机制。这些机制不是不好但就像用螺丝刀拧所有螺丝有些场景下显得笨重且效率不高。JUC提供了一系列更精细、更高效、功能更强大的工具比如各种锁ReentrantLock, ReadWriteLock、线程安全的集合ConcurrentHashMap, CopyOnWriteArrayList、线程池ThreadPoolExecutor、以及用于协调线程的同步器CountDownLatch, CyclicBarrier, Semaphore等。彻底搞懂JUC意味着你能根据不同的业务场景比如缓存更新、订单处理、批量任务执行精准地选用最合适的并发组件写出既安全线程安全又高效高吞吐、低延迟的代码。2. JUC核心组件深度解析与设计思想要彻底搞懂JUC不能只停留在会用的层面必须理解其背后的设计思想和每个核心组件的适用场景。JUC的设计哲学可以概括为“分离关注点”和“提供更细粒度的控制”。它将同步、互斥、通信等概念抽象成一个个独立的组件让我们可以像搭积木一样构建复杂的并发程序。2.1 锁机制从synchronized到Lock的进化synchronized是Java原生的互斥同步关键字它简单易用JVM会负责加锁和释放锁。但在复杂的场景下它暴露出几个局限性1等待锁的线程无法被中断2尝试获取锁时如果失败会一直阻塞无法设置超时3锁的获取和释放必须在一个代码块或方法内不够灵活。JUC中的Lock接口及其实现类主要是ReentrantLock就是为了解决这些问题而生的。ReentrantLock提供了synchronized不具备的三大能力可中断的锁获取通过lockInterruptibly()方法在等待锁的过程中线程可以响应中断信号这有助于避免死锁或实现更优雅的线程终止逻辑。可超时的锁获取通过tryLock(long time, TimeUnit unit)方法可以指定一个等待时间。如果在规定时间内没拿到锁线程不会无限期阻塞而是返回false程序可以执行其他备选逻辑。公平锁与非公平锁ReentrantLock的构造函数可以传入一个boolean参数指定是创建公平锁还是非公平锁。公平锁保证等待时间最长的线程优先获取锁避免了“饥饿”现象但会带来更多的线程切换开销吞吐量通常较低。非公平锁则允许“插队”吞吐量高是默认且更常用的选择。实操心得在绝大多数高并发场景下请直接使用非公平锁。公平锁带来的额外性能开销在竞争激烈时非常明显。只有在你非常确定需要绝对公平性且性能不是首要考量时才考虑公平锁。除了基本的互斥锁ReadWriteLock读写锁是另一个极其重要的锁工具。它的设计思想是“读读不互斥读写互斥写写互斥”。这对于“读多写少”的场景比如缓存是巨大的性能优化。多个线程可以同时读数据只有写操作才会独占锁。JUC提供了ReentrantReadWriteLock作为其实现。2.2 原子类无锁并发编程的利器在多线程环境下即便是i这样的操作也不是原子的因为它涉及读取、计算、写入三个步骤。传统的做法是给整个方法或代码块加锁但这在竞争激烈时性能损耗大。JUC的原子类如AtomicInteger,AtomicLong,AtomicReference基于CASCompare-And-Swap操作实现。CAS是一种乐观锁机制它包含三个操作数内存位置V、预期原值A和新值B。当且仅当V的值等于A时处理器才会用B更新V的值否则不执行更新。整个操作是一个原子指令通常由CPU硬件层面保证。// 传统方式 private int count 0; public synchronized void increment() { count; } // 使用AtomicInteger private AtomicInteger count new AtomicInteger(0); public void increment() { count.incrementAndGet(); // 底层使用CAS无需显式加锁 }原子类的优势在于它在低至中度竞争环境下性能远高于锁。因为它避免了线程挂起和上下文切换的开销。但是在高竞争环境下CAS可能因为反复失败而进入“自旋”状态消耗CPU资源。这时可能需要结合其他同步策略。2.3 并发容器线程安全的数据结构直接在多线程环境下使用ArrayList、HashMap等非线程安全容器是灾难性的。虽然可以用Collections.synchronizedList包装但这种粗粒度的同步方式性能很差。JUC提供了一系列高性能的并发容器ConcurrentHashMap这是最重要的并发容器。在JDK 1.7及之前它采用“分段锁”机制将数据分成一段一段的每段配一把锁这样不同段的操作可以并发。在JDK 1.8及之后它做了巨大优化大量使用synchronized和CAS来锁住单个链表头或红黑树根节点并发粒度更细性能更高。它是替代Hashtable和同步包装的HashMap的首选。CopyOnWriteArrayList和CopyOnWriteArraySet采用“写时复制”策略。任何修改操作add, set, remove都会在底层创建一个新的数组副本在新副本上操作完成后再将引用指向新数组。读操作则完全无锁。这非常适合“读多写极少”的场景比如监听器列表。需要注意的是写操作开销大且会占用双倍内存不适合频繁修改的场景。ConcurrentLinkedQueue一个基于链接节点的无界线程安全队列。它采用“非阻塞”算法CAS提供了高效的并发入队和出队操作。2.4 线程池管理线程的生命周期直接new Thread()创建线程然后start()在需要处理大量短期异步任务的场景下如Web服务器处理请求会导致频繁创建和销毁线程消耗大量系统资源。线程池的核心思想是“池化技术”预先创建好一些线程放在“池”里来任务时从池中取线程执行执行完毕后再放回池中从而避免频繁创建销毁的开销。JUC的线程池核心是ThreadPoolExecutor类。理解它的构造参数是正确使用的关键public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)corePoolSize核心线程数。即使线程空闲也会保留在池中的线程数量除非设置了allowCoreThreadTimeOut。maximumPoolSize最大线程数。池中允许存在的最大线程数。keepAliveTime空闲线程存活时间。当线程数超过核心线程数时多余的空闲线程在等待新任务时的最长存活时间。workQueue工作队列。用于存放等待执行的任务。常用的有LinkedBlockingQueue无界队列、ArrayBlockingQueue有界队列、SynchronousQueue直接交接队列。handler拒绝策略。当线程池和队列都满了如何处理新提交的任务。内置策略有AbortPolicy直接抛出异常、CallerRunsPolicy由调用者线程执行、DiscardPolicy直接丢弃、DiscardOldestPolicy丢弃队列中最老的任务。注意事项使用Executors工厂类如newFixedThreadPool,newCachedThreadPool创建线程池虽然方便但隐藏了参数细节容易导致问题如newFixedThreadPool使用无界队列可能堆积大量任务导致OOM。生产环境建议直接使用ThreadPoolExecutor构造函数根据业务特点明确配置各个参数。2.5 同步器线程间的协调工具有时候我们需要的不是互斥访问而是让多个线程在某个点上“汇合”或按某种顺序执行。JUC提供了几个强大的同步器CountDownLatch倒计时闩锁一个线程或多个等待其他一组线程完成操作。构造时指定一个计数N。其他线程完成任务后调用countDown()计数减1。等待的线程调用await()会阻塞直到计数减为0。它是一次性的计数归零后无法重置。典型场景主线程等待所有子线程初始化完成后再执行。CyclicBarrier循环栅栏让一组线程互相等待直到所有线程都到达一个公共屏障点然后屏障打开所有线程继续执行并且屏障可以重置复用。构造时指定参与线程数N。每个线程调用await()表示到达屏障并阻塞。当第N个线程调用await()后所有被阻塞的线程被唤醒继续执行。它适用于多轮迭代计算的场景。Semaphore信号量用来控制同时访问特定资源的线程数量。它维护了一组“许可证”。线程执行前需要调用acquire()获取许可证如果无证可用则阻塞执行完后调用release()归还许可证。常用于流量控制如数据库连接池限流。Exchanger交换器用于两个线程之间交换数据。每个线程在调用exchange()方法时会阻塞直到另一个线程也调用此方法然后双方交换数据并继续执行。3. 核心场景实战与避坑指南理论懂了还得在实战中锤炼。下面我们通过几个典型场景看看如何组合运用JUC工具。3.1 场景一构建一个高性能的本地缓存假设我们要实现一个商品信息的本地缓存要求是读非常频繁写相对较少比如商品信息每天只更新一两次。错误做法使用HashMap并用synchronized包装所有方法。这会导致所有读操作也串行化性能极差。正确做法使用ReadWriteLock或直接使用ConcurrentHashMap。方案AReadWriteLock适合缓存逻辑比较复杂需要手动控制读写锁的范围。public class ProductCacheWithRWLock { private final MapString, Product cache new HashMap(); private final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); public Product getProduct(String id) { rwLock.readLock().lock(); try { return cache.get(id); } finally { rwLock.readLock().unlock(); } } public void updateProduct(String id, Product product) { rwLock.writeLock().lock(); try { cache.put(id, product); } finally { rwLock.writeLock().unlock(); } } }方案BConcurrentHashMap更简单直接get操作完全无锁put操作有细粒度锁性能极高是首选。public class ProductCacheWithCHM { private final ConcurrentHashMapString, Product cache new ConcurrentHashMap(); public Product getProduct(String id) { return cache.get(id); // 无锁 } public void updateProduct(String id, Product product) { cache.put(id, product); // 细粒度锁 } }避坑点这里有一个经典的“缓存穿透”问题。如果查询一个不存在的商品IDget会返回null。高并发下大量请求同时查询同一个不存在的ID会导致请求都穿透缓存打到数据库。解决方法可以是缓存空对象null或特殊标记或者使用布隆过滤器Bloom Filter预先过滤。3.2 场景二批量任务执行与结果汇总主线程需要发起100个并行任务比如调用100个外部接口等所有任务执行完毕后汇总结果进行下一步处理。实现使用CountDownLatch或CompletableFutureJDK8。使用CountDownLatchpublic class BatchTaskWithLatch { public void executeBatchTasks() throws InterruptedException { int taskCount 100; CountDownLatch latch new CountDownLatch(taskCount); ExecutorService executor Executors.newFixedThreadPool(10); ListFutureResult futures new ArrayList(); for (int i 0; i taskCount; i) { FutureResult future executor.submit(() - { try { // 执行单个任务 return doTask(); } finally { latch.countDown(); // 任务完成计数减1 } }); futures.add(future); } latch.await(); // 主线程等待所有任务完成 System.out.println(所有任务已完成开始汇总...); // 汇总Future结果 ListResult results new ArrayList(); for (FutureResult future : futures) { results.add(future.get()); } // ... 处理汇总结果 executor.shutdown(); } }使用CompletableFuture更现代功能更强public class BatchTaskWithCF { public void executeBatchTasks() { ListCompletableFutureResult futures new ArrayList(); for (int i 0; i 100; i) { futures.add(CompletableFuture.supplyAsync(() - doTask())); } // 等待所有Future完成 CompletableFutureVoid allFutures CompletableFuture.allOf( futures.toArray(new CompletableFuture[0]) ); // 所有任务完成后获取结果 CompletableFutureListResult resultFuture allFutures.thenApply(v - futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()) ); ListResult results resultFuture.join(); // 阻塞直到获取结果 // ... 处理汇总结果 } }避坑点使用CountDownLatch时务必在finally块中调用countDown()确保任务无论成功失败计数器都能递减避免主线程永远等待。使用线程池时任务抛出的异常会被封装在Future里调用future.get()时才会抛出需要注意异常处理。3.3 场景三实现一个简单的连接池模拟一个数据库连接池最大连接数为10。当线程需要连接时从池中获取如果池已空则等待使用完毕后归还连接。实现使用Semaphore作为许可证控制器配合一个线程安全的队列存放连接对象。public class SimpleConnectionPool { private final int poolSize 10; // 使用信号量控制并发获取连接的数量 private final Semaphore available new Semaphore(poolSize, true); // 使用阻塞队列存放空闲连接 private final BlockingQueueConnection pool new ArrayBlockingQueue(poolSize); public SimpleConnectionPool() { for (int i 0; i poolSize; i) { pool.offer(createConnection()); } } public Connection getConnection() throws InterruptedException { available.acquire(); // 获取许可证如果没有则阻塞 Connection conn pool.poll(); // 从池中取出一个连接 return conn; } public void releaseConnection(Connection conn) { if (conn ! null) { pool.offer(conn); // 归还连接到池中 available.release(); // 释放许可证 } } private Connection createConnection() { // 模拟创建连接 return new MockConnection(); } }避坑点这是一个简化模型。真实的连接池如HikariCP要考虑更多连接有效性检测心跳、空闲连接超时回收、获取连接超时控制、连接泄漏检测等。Semaphore在这里只是控制了“入口”的并发数连接对象本身的管理还需要依赖队列和其他机制。4. 高级主题与性能调优考量当你掌握了基础组件的使用后就需要关注一些更深入的话题这些往往在线上问题排查和性能优化时至关重要。4.1 锁的性能与优化锁是性能的敌人但又是保证正确性的必要手段。优化锁的宗旨是减少锁的粒度、缩短锁的持有时间、降低锁的竞争频率。锁粗化 vs 锁细化如果一段代码中频繁地对同一个锁进行“加锁-释放”操作比如在循环内JVM可能会自动进行“锁粗化”将多次锁合并为一次以减少开销。相反我们编码时应尽量“锁细化”只锁住必要的共享数据部分而不是锁住整个方法或大段代码。锁消除JVM的即时编译器JIT会进行逃逸分析。如果发现某个锁对象不可能被其他线程访问到即不会发生共享就会将这个锁消除。这提示我们应尽量限制变量的作用域。使用无锁数据结构在可能的情况下优先考虑使用原子类AtomicInteger或ConcurrentHashMap这类基于CAS的无锁或细粒度锁结构它们在高并发读场景下优势明显。避免热点竞争像AtomicLong这样的原子类在高并发更新时所有线程都在竞争同一个变量地址CAS失败率会很高。JDK 8提供了LongAdder和DoubleAdder来解决这个问题。它们内部将一个变量拆分成多个Cell线程更新时分散到不同的Cell上最后再汇总极大地减少了竞争在超高并发写场景下性能远超AtomicLong。4.2 线程池的合理配置与监控线程池配置不当是线上服务不稳定的常见原因。这里有几个经验法则任务类型决定池类型CPU密集型计算、处理线程数建议设置为CPU核心数 1。过多线程会导致频繁的上下文切换反而降低性能。I/O密集型网络请求、数据库操作线程数可以设置得多一些因为线程大部分时间在等待I/O。经验公式线程数 CPU核心数 * (1 平均等待时间 / 平均计算时间)。这个比例通常称为阻塞系数在2~10之间都是常见的。例如一个主要调用外部API的服务可以设置为核心数 * 4或更多。队列选择LinkedBlockingQueue无界如果任务提交速度偶尔超过处理速度队列可以起到缓冲作用。但风险是如果任务产生速度持续高于处理速度队列会无限增长最终导致OOM。慎用。ArrayBlockingQueue有界可以防止资源耗尽。当队列满时会根据拒绝策略处理。需要合理评估队列容量。SynchronousQueue不存储元素提交的任务直接交给线程执行如果没有空闲线程则创建新线程未达最大线程数时或执行拒绝策略。这要求线程池有足够的增长能力适合任务量不大但要求低延迟的场景。监控与动态调整Spring Boot的Actuator可以暴露线程池指标。更高级的做法是使用像Hystrix已停更或Resilience4j的线程池隔离或者使用可以动态调整参数的线程池如美团开源的DynamicTp。4.3 常见并发问题与排查技巧即使使用了JUC如果使用不当依然会产生问题。下面是一个快速排查表问题现象可能原因排查思路与解决方案CPU占用率过高1. 死循环2. 大量线程处于可运行状态Runnable激烈竞争CPU3. 大量自旋如CAS失败频繁。1. 使用jstack或Arthas查看线程栈找到占用CPU高的线程。2. 检查是否有不当的循环或锁竞争。3. 对于原子类考虑是否竞争过于激烈可换用LongAdder。响应缓慢吞吐量低1. 锁竞争激烈大量线程阻塞BLOCKED。2. I/O等待。3. 线程池配置不合理如核心线程数过少队列过长。1.jstack查看线程状态统计BLOCKED线程数。2. 使用jstat或可视化工具查看GC情况排除Full GC。3. 检查线程池监控指标调整核心/最大线程数和队列类型。内存溢出OOM1. 使用无界队列如LinkedBlockingQueue的线程池任务堆积。2.ThreadLocal使用后未清理导致内存泄漏尤其在线程池中线程是复用的。1. 检查线程池队列大小。2. 使用内存分析工具如MAT分析堆转储查看占据内存最大的对象。3. 确保ThreadLocal使用后调用remove()。数据不一致1. 存在“先检查后执行”的非原子复合操作Check-Then-Act。2. 误以为单个操作是原子的如ConcurrentHashMap的putIfAbsent是原子的但get后再put不是。1. 使用锁或原子操作如ConcurrentHashMap的computeIfAbsent将复合操作保护起来。2. 仔细阅读API文档确认操作的原子性边界。死锁多个线程互相持有对方需要的锁并等待对方释放。1.jstack可以检测到死锁并报告。2. 编码时遵循固定的锁获取顺序。3. 使用tryLock设置超时超时后释放已持有的锁并重试或记录日志告警。排查工具推荐命令行jps,jstack,jstat,jmap。这是最基础也是最强大的工具。可视化/APMArthas阿里开源功能强大JConsole, VisualVM, Prometheus Grafana监控指标。压测JMeter,iperf网络打流但可类比理解系统压力测试。5. 从JUC到更高阶的并发模型彻底搞懂JUC是构建稳健高并发应用的坚实基础。但技术总是在演进。在现代Java开发中特别是响应式编程和异步处理领域有一些建立在JUC之上或与之互补的技术值得关注CompletableFuture(JDK 8): 它不仅仅是Future的增强版更代表了一种异步编程的范式。它允许你以声明式的、链式调用的方式组合多个异步任务处理它们的完成、异常和结果转换大大简化了复杂的异步流程编排代码。它内部同样使用了ForkJoinPool等线程池。响应式编程 (Reactor, RxJava): 在处理大量并发I/O操作如微服务间调用、消息推送时传统的阻塞式线程模型一个请求一个线程会消耗大量线程资源。响应式编程基于事件驱动和异步非阻塞用更少的资源甚至单线程处理更高的并发。Spring WebFlux就是基于Reactor实现的。它的底层也依赖于JUC中的调度器Schedulers。协程 (Project Loom - 预览特性): 这是Java未来可能引入的“轻量级线程”虚拟线程。它由JVM管理创建和切换开销极低可以创建数十万甚至数百万个而不会导致系统资源耗尽。这有望从根本上简化高并发编程模型让我们可以用接近同步编程的写法一个请求一个虚拟线程获得异步非阻塞的性能。虽然还未正式发布但值得保持关注。我个人在实际项目中的体会是JUC是并发世界的“普通话”必须流利掌握。而CompletableFuture和响应式编程更像是“方言”或“专业术语”在特定的场景如全链路异步、流处理下能发挥巨大威力。我的建议是先扎实练好JUC这套基本功理解线程、锁、同步的本质。当你能熟练运用ExecutorService、ConcurrentHashMap、CountDownLatch等工具解决日常并发问题后再去学习CompletableFuture的链式调用和响应式编程的背压、流控制等概念就会水到渠成发现它们不过是更高层次的抽象和封装。最终你会形成自己的并发工具箱面对不同的业务场景能够自信地选出最趁手的那把“武器”。