
1. 项目概述为什么JUC是Java工程师的必修课如果你是一名Java开发者尤其是工作一两年后开始接触稍微复杂点的业务系统那么“并发”这个词一定会频繁地出现在你的视野里。订单超卖、库存扣减变成负数、用户积分莫名其妙被重复增加……这些让人头疼的线上问题其根源往往都指向并发编程的缺陷。而Java世界里处理这些并发难题的核心武器库就是JUCjava.util.concurrent。这个项目标题“【JUC基础学习笔记】”看似简单背后却是一个Java工程师从“能写代码”到“能写好代码”的关键分水岭。它不是一个简单的API使用手册而是一套关于如何安全、高效地组织多线程任务的思维体系和工程实践。我刚开始工作时对synchronized关键字一知半解天真地以为加上它就能解决所有并发问题结果在复杂的业务场景下踩了无数坑死锁让服务卡死、性能瓶颈导致接口超时、诡异的线程安全问题只在生产环境出现。直到系统性地啃下JUC才真正理解了锁的本质、线程协作的奥妙以及如何设计出高并发的数据结构。这份学习笔记就是我结合多年实战和面试官视角为你梳理的一条从入门到理解的清晰路径。无论你是正在准备面试被“AQS”、“CAS”、“线程池七大参数”等问题所困扰还是在实际开发中遇到了并发瓶颈这份笔记都能为你提供直击要害的解析和可落地的方案。2. JUC核心框架与设计思想拆解2.1 JUC的三大支柱原子类、锁与并发容器JUC包虽然庞大但其核心设计思想可以归结为三大支柱理解了它们就抓住了JUC的命脉。第一支柱原子变量类Atomic*。这是理解现代并发编程的起点。传统的i操作在多线程下是非原子的因为它包含“读-改-写”三个步骤极易出现丢失更新。JUC提供了一系列如AtomicInteger、AtomicReference等类它们背后的核心技术是CASCompare-And-Swap**。你可以把CAS想象成一个乐观的仲裁者它认为冲突不常发生。当线程要更新一个值时它会带上两个信息——期望的旧值我认为这个变量现在应该是A和想要设置的新值我想把它改成B。CAS操作会检查当前值是否真的为A如果是则原子性地更新为B如果不是说明在我“认为”的期间已经有其他线程修改了它那么本次更新失败线程通常会重试。这种无锁Lock-Free机制避免了线程挂起和唤醒的巨大开销是构建高性能并发组件的基础。java.util.concurrent.atomic包下的LongAdder在高争用场景下比AtomicLong性能更优正是因为它采用了分段CAS的思想分散了竞争热点。第二支柱锁与同步器Locks Synchronizers。这是JUC最复杂也最精华的部分。内置锁synchronized简单易用但功能单一不可中断、非公平、不支持多条件等待。JUC的Lock接口主要实现ReentrantLock提供了更灵活的锁操作可中断地获取锁、尝试非阻塞获取tryLock、公平/非公平策略选择。但更重要的是其基石——AQSAbstractQueuedSynchronizer抽象队列同步器。AQS是JUC中大部分同步工具如ReentrantLock、CountDownLatch、Semaphore的“心脏”。它内部维护了一个双向链表的同步队列CLH变体和一个状态变量state。当线程获取锁失败时会被包装成一个节点Node加入队列尾部并进入等待状态当持有锁的线程释放锁时会唤醒队列中的下一个线程。AQS通过模板方法模式将具体的资源获取与释放逻辑交给子类实现如ReentrantLock中state0表示锁空闲state0表示被重入自己则负责复杂的线程排队、阻塞与唤醒机制。理解AQS就等于拿到了解开JUC同步器家族所有秘密的万能钥匙。第三支柱并发容器Concurrent Collections。直接使用HashMap、ArrayList在多线程环境下是灾难性的。JUC提供了一套线程安全的容器其实现策略主要分两类写时复制Copy-On-Write如CopyOnWriteArrayList。任何写操作增、删、改都会先复制底层数组在新数组上操作完成后用新数组替换旧引用。读操作完全无锁性能极高。适用于读多写极少的场景如监听器列表、黑白名单。如果写操作频繁频繁的数组复制会导致性能下降和内存占用激增。分段锁/ CAS优化如ConcurrentHashMap。在Java 7中它采用分段锁Segment将数据分成一段一段的每段一把锁提升了并发度。在Java 8及以后它进行了大规模重构底层采用Node数组链表/红黑树锁的粒度细化到了链表头节点或树根节点并且大量使用synchronized和CAS来实现更精细的并发控制性能得到了质的飞跃。2.2 从synchronized到ReentrantLock锁的进化论很多初学者会问有了synchronized为什么还要ReentrantLock这不仅仅是功能上的补充更是设计哲学的不同。synchronized是JVM层面的内置锁使用简单由JVM负责加锁、释放锁以及线程阻塞和唤醒。它是非公平锁且不可中断一个线程在等待锁时无法被外部中断也不支持多个等待条件。它的锁信息存储在对象头中优化历程包括了偏向锁、轻量级锁、重量级锁的升级过程目的是在无竞争或低竞争时减少开销。而ReentrantLock是JDK层面的一个类提供了更丰富的API和灵活性可中断锁lockInterruptibly()方法允许在等待锁的过程中响应中断这对于实现可取消的任务非常重要。尝试非阻塞获取锁tryLock()方法可以立即返回获取结果避免线程无限期等待可以用来解决死锁问题尝试获取所有锁失败则释放已获得的。公平锁选项构造函数传入true可以创建公平锁保证等待时间最长的线程优先获取锁避免了线程饥饿但会带来更大的性能开销上下文切换更多。多个条件变量Condition一个Lock可以绑定多个Condition对象实现更精细的线程等待/通知。典型场景是“生产者-消费者”模型可以用两个Condition分别管理队列“非满”和“非空”的等待线程比Object.wait()/notifyAll()更高效、更不易出错。实操心得在绝大多数情况下优先使用synchronized。它的语法简洁性能在JDK不断优化下已经非常优秀且不容易出现忘记解锁ReentrantLock必须在finally中unlock()的问题。只有当你的业务场景明确需要上述ReentrantLock独有的特性时才考虑使用它。不要为了“炫技”而使用更复杂的工具。2.3 线程池不要重复发明轮子尤其是“坏轮子”“我需要处理一批任务那就new Thread()吧”——这是并发编程中最危险的念头之一。线程的创建和销毁成本很高无限制地创建线程会耗尽系统资源。线程池ThreadPoolExecutor就是管理线程生命周期的“专家”。理解线程池关键在于掌握其七大核心参数corePoolSize核心线程数线程池中长期存活的线程数量即使它们空闲。maximumPoolSize最大线程数线程池允许创建的最大线程数量。workQueue工作队列用于存放待执行任务的阻塞队列。keepAliveTime空闲线程存活时间当线程数超过核心线程数时多余的空闲线程在销毁前等待新任务的最长时间。unit时间单位keepAliveTime的时间单位。threadFactory线程工厂用于创建新线程可以定制线程名、优先级、守护状态等便于监控和排查问题。handler拒绝策略当线程池和队列都已满如何处理新提交的任务。内置策略有AbortPolicy抛出异常、CallerRunsPolicy由调用者线程执行、DiscardOldestPolicy丢弃队列中最老的任务、DiscardPolicy直接丢弃。线程池的工作流程就像一个银行办事大厅核心线程数 常开的服务窗口。工作队列 大厅里的等候区座位。最大线程数 所有能开的服务窗口包含临时加开的。拒绝策略 当窗口全开、座位全满时保安采取的措施如告知客户明日再来。任务提交后先看核心窗口是否空闲是则立即办理否则任务去等候区排队如果等候区也满了就尝试加开临时窗口直到达到最大窗口数如果窗口已开到最大且等候区满则触发拒绝策略。避坑指南Executors工厂类提供的newFixedThreadPool、newCachedThreadPool等方法虽然方便但隐藏了参数细节容易导致问题如newCachedThreadPool队列无限小可能创建大量线程newFixedThreadPool使用无界队列可能堆积任务导致OOM。生产环境建议使用ThreadPoolExecutor构造函数手动创建根据业务特点CPU密集型、IO密集型合理配置参数。监控线程池的运行状态任务数、活跃线程数、队列大小也是线上稳定的关键。3. 核心工具类实战解析与应用场景3.1 同步辅助三剑客CountDownLatch、CyclicBarrier、Semaphore这些工具基于AQS构建用于解决特定的线程协作问题比直接用wait/notify更安全、更清晰。CountDownLatch倒计时闩锁像一个计数器构造时设定一个数字N。主线程调用await()等待其他工作线程完成任务后调用countDown()将计数器减1。当计数器减到0时所有在await()处等待的线程被唤醒。它是一次性的用完即废。典型场景主线程等待多个子线程完成数据加载、初始化等准备工作后再继续执行。例如启动一个服务需要等待数据库连接池、缓存客户端、消息队列消费者等都初始化完毕。CyclicBarrier循环栅栏让一组线程相互等待到达一个共同的屏障点Barrier后再同时继续执行。构造时也需要一个数字N参与方数量以及一个可选的Runnable屏障动作当所有线程到达后由最后一个到达的线程执行。await()表示“我已到达屏障”。与CountDownLatch的区别CyclicBarrier是可重置循环使用的且强调的是多个工作线程之间的相互等待而CountDownLatch是主等从且一次性。典型场景多线程计算任务将一个大任务拆分成多个子任务所有子任务都计算完成后再合并结果。或者模拟多玩家游戏的回合制开始。Semaphore信号量用来控制同时访问特定资源的线程数量。它维护了一组“许可证”permits。线程通过acquire()获取一个许可证如果没有则阻塞访问完资源后通过release()归还许可证。典型场景数据库连接池限流、限制某个接口的并发调用数。你可以把它理解成一个“停车场空位计数器”车位满了许可证发完新车就得在外面排队。3.2 Future与CompletableFuture异步编程的利器Runnable没有返回值Callable有返回值。但如何获取一个在另一个线程中执行的任务的结果呢答案是Future。Future代表一个异步计算的结果。你提交一个Callable任务给线程池会立即得到一个Future对象。之后你可以通过future.get()来阻塞地获取计算结果或者用future.isDone()来轮询任务是否完成。get方法的问题是它会阻塞如果任务执行时间很长调用线程就会被挂起这不够灵活。CompletableFuture是Java 8引入的更强大的未来它实现了Future和CompletionStage接口。它的核心魅力在于链式异步编程和组合式编程。链式调用你可以在一个异步任务完成后自动触发下一个任务而无需手动调用get()阻塞等待。CompletableFuture.supplyAsync(() - fetchPriceFromAPI(productId)) // 异步获取价格 .thenApply(price - calculateTax(price)) // 然后同步计算税费 .thenAccept(totalPrice - sendNotification(totalPrice)) // 最后消费结果 .exceptionally(ex - { // 异常处理 log.error(Calculation failed, ex); return defaultPrice; });组合多个FuturethenCombine可以将两个独立的CompletableFuture的结果进行合并allOf/anyOf可以等待所有/任意一个Future完成。手动完成你可以通过complete(T value)或completeExceptionally(Throwable ex)手动设置一个CompletableFuture的结果这在超时控制等场景非常有用。CompletableFuture将复杂的异步回调地狱Callback Hell变成了清晰的流水线Pipeline是现代响应式编程的基础。3.3 ThreadLocal原理与内存泄漏防范ThreadLocal并非用于解决共享变量并发访问问题而是为了让每个线程都能拥有一个该变量的独立副本从而避免线程间相互干扰。它的常见应用场景包括存储用户会话信息如当前登录用户ID在Web应用的整个请求链路中传递。数据库连接或事务上下文管理。避免在方法间传递繁琐的参数。其原理是每个Thread对象内部都有一个ThreadLocalMap类型的变量threadLocals。这个Map的Key是ThreadLocal实例本身弱引用Value是线程独有的变量副本。当你在一个线程中调用threadLocal.set(value)时实际上是在当前线程的threadLocals这个Map里以当前threadLocal对象为Key存入了value。内存泄漏风险是ThreadLocal最著名的坑。由于ThreadLocalMap的Key是弱引用当ThreadLocal实例失去强引用比如被置为null后在GC时这个Key会被回收但对应的Value是强引用只要线程还在运行例如使用线程池线程会复用这个Value就永远不会被回收造成内存泄漏。防范措施养成良好习惯每次使用完ThreadLocal后必须调用remove()方法清理当前线程的Value。尤其是在使用线程池时线程是复用的上一个任务留下的Value会污染下一个任务。将ThreadLocal变量声明为static final这减少了创建多个ThreadLocal实例的可能并且由于是静态的其生命周期与类一致通常不会被随意置null降低了Key被回收的风险。但remove()依然必不可少。4. 并发编程实战从理论到代码的跨越4.1 设计一个线程安全的缓存从简单到高效让我们通过一个逐步优化的例子将前面提到的知识串联起来。需求实现一个计算耗时的方法的缓存。版本1最朴素的synchronized方法public class SynchronizedCache { private final MapString, String cache new HashMap(); public synchronized String getData(String key) { String result cache.get(key); if (result null) { result expensiveCompute(key); // 模拟耗时计算 cache.put(key, result); } return result; } }问题锁粒度太粗整个方法被锁住所有线程串行访问即使计算不同key的数据也要等待性能极差。版本2双重检查锁定DCL与ConcurrentHashMappublic class ConcurrentCache { private final ConcurrentHashMapString, String cache new ConcurrentHashMap(); public String getData(String key) { String result cache.get(key); if (result null) { synchronized (this) { // 细粒度锁只锁当前key相关的代码块 result cache.get(key); // 再次检查双重检查 if (result null) { result expensiveCompute(key); cache.put(key, result); } } } return result; } }优化使用ConcurrentHashMap保证基础get操作的线程安全并使用synchronized块对“计算并放入缓存”这个复合操作加锁。双重检查避免了在锁内首次检查后多个线程仍重复计算的问题。但synchronized块在超高并发下仍有竞争。版本3使用ConcurrentHashMap的computeIfAbsent方法Java 8public class ComputeIfAbsentCache { private final ConcurrentHashMapString, String cache new ConcurrentHashMap(); public String getData(String key) { return cache.computeIfAbsent(key, k - expensiveCompute(k)); } }这是推荐做法。computeIfAbsent方法是原子性的如果Key不存在则使用提供的函数计算Value并放入Map然后返回该Value如果Key已存在则直接返回现有Value。其内部实现了更细粒度的锁或CAS操作性能更好代码也更简洁。但需要注意传入的映射函数expensiveCompute应尽量简短且无副作用因为它可能在锁内执行。4.2 生产者-消费者模式的多种实现这是并发编程的经典模型用于解耦生产数据和消费数据的速率不匹配问题。实现1使用Object.wait()/notifyAll()这是最基础的实现需要手动管理等待条件队列“非满”和“非空”代码繁琐且容易出错比如错误使用notify导致线程无法被唤醒。实现2使用BlockingQueue这是最简单且推荐的方式。BlockingQueue如ArrayBlockingQueue、LinkedBlockingQueue本身就是一个线程安全的队列并且提供了阻塞的put队列满时阻塞和take队列空时阻塞方法。BlockingQueueTask queue new LinkedBlockingQueue(100); // 容量100 // 生产者线程 queue.put(new Task(...)); // 消费者线程 Task task queue.take(); process(task);JUC已经帮你处理了所有的锁和条件等待你只需要关注业务逻辑。实现3使用ReentrantLock和Condition如果你想更深入地理解等待/通知机制或者有更复杂的条件判断可以手动实现public class CustomBlockingQueueT { private final LinkedListT list new LinkedList(); private final int capacity; private final Lock lock new ReentrantLock(); private final Condition notFull lock.newCondition(); // 队列“未满”条件 private final Condition notEmpty lock.newCondition(); // 队列“非空”条件 public void put(T item) throws InterruptedException { lock.lock(); try { while (list.size() capacity) { // 一定要用while循环检查防止虚假唤醒 notFull.await(); } list.add(item); notEmpty.signal(); // 唤醒一个等待“非空”的消费者 } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (list.isEmpty()) { notEmpty.await(); } T item list.removeFirst(); notFull.signal(); // 唤醒一个等待“未满”的生产者 return item; } finally { lock.unlock(); } } }这个实现清晰地展示了如何用两个Condition对象精确控制生产者和消费者线程的等待与唤醒比单一的wait/notifyAll更高效。5. 高级主题与性能调优洞察5.1 Java内存模型JMM与Happens-Before原则并发Bug之所以难查很多时候是因为它们具有“不确定性”和“难以复现”的特点。其根源在于Java内存模型JMM定义的线程工作内存与主内存的交互规则。JMM规定每个线程有自己的工作内存存储了该线程使用到的变量的副本。所有变量都存储在主内存中。线程对变量的所有操作读、写都必须在工作内存中进行不能直接读写主内存。不同线程之间无法直接访问对方工作内存中的变量线程间变量值的传递需要通过主内存来完成。这导致了可见性问题线程A修改了共享变量但修改后的值可能还停留在自己的工作内存没有及时写回主内存线程B就无法看到这个修改。synchronized和volatile关键字以及final域它们的底层语义很大一部分就是为了解决可见性和有序性问题。Happens-Before是JMM定义的一个偏序关系它规定了哪些操作必须对其他操作可见。它是判断线程间操作是否存在竞争、数据是否安全的核心准则。几条关键的Happens-Before规则包括程序次序规则一个线程内按照代码顺序前面的操作Happens-Before于后面的操作。管程锁定规则一个unlock操作Happens-Before于后续对同一个锁的lock操作。volatile变量规则对一个volatile变量的写操作Happens-Before于后续对这个变量的读操作。线程启动规则Thread.start()调用Happens-Before于这个线程内的任何操作。线程终止规则线程中的所有操作都Happens-Before于其他线程检测到该线程已经终止如Thread.join()成功返回。传递性如果A Happens-Before B且B Happens-Before C那么A Happens-Before C。理解Happens-Before你就能明白为什么volatile能保证可见性因为它建立了写-读的H-B关系为什么synchronized块内的操作对退出块后的操作可见管程锁定规则。5.2 锁优化与性能考量在高并发场景下锁的竞争会成为系统瓶颈。除了选择更高效的锁如ReentrantLock的非公平模式、StampedLock的乐观读还有一些编码层面的优化技巧减小锁的粒度将一个大锁拆分成多个小锁。最经典的例子就是ConcurrentHashMap从分段锁Java 7到桶级别锁Java 8的演进。在业务代码中可以思考是否可以对不同的数据块使用不同的锁对象而不是锁住整个方法或整个对象。缩短锁的持有时间只在必须保证原子性的代码段加锁。锁内只做必要的操作将耗时的IO操作、复杂计算等尽可能移到锁外。锁分离根据读写操作的特点进行分离。ReadWriteLock读写锁允许多个线程同时读但写操作是独占的。这在读多写少的场景如缓存下能极大提升吞吐量。Java 8引入了性能更好的StampedLock它提供了乐观读模式在读多写少的情况下几乎完全无锁。无锁编程这是性能的终极追求。完全依靠CAS操作如原子类或不可变对象来实现线程安全。java.util.concurrent.atomic包下的AtomicIntegerFieldUpdater等工具可以在不创建原子类对象的情况下直接更新某个类的volatile字段进一步减少开销。避免锁的嵌套这是死锁的温床。如果必须使用多个锁务必保证所有线程以固定的全局顺序获取锁例如按锁对象的哈希值排序这是预防死锁最有效的方法之一。5.3 并发调试与问题排查实战记录并发问题难以复现因此日志和监控至关重要。以下是一些实战中总结的排查技巧线程堆栈分析当应用出现卡顿、CPU飙高但吞吐量低时首先使用jstack命令或Arthas等工具抓取线程堆栈。重点查看大量线程是否阻塞在同一个锁上BLOCKED状态这可能意味着锁竞争激烈。是否有线程处于WAITING或TIMED_WAITING状态是在等什么条件Object.wait()Condition.await()LockSupport.park()。是否存在死锁jstack输出中会明确提示“Found one Java-level deadlock”。善用ThreadLocal存储诊断信息在请求入口处将唯一的追踪ID如TraceId存入ThreadLocal在日志框架如Logback、Log4j2的Pattern中配置输出该ID。这样同一个请求在所有微服务、所有线程中的日志都能被串联起来对排查复杂的并发调用链问题有奇效。模拟与测试使用CountDownLatch或CyclicBarrier在单元测试中精确控制多个线程的执行时序模拟并发场景。虽然不能覆盖所有情况但能帮助发现一些明显的竞态条件。监控线程池通过ThreadPoolExecutor的getQueue().size()、getActiveCount()、getCompletedTaskCount()等方法或通过Spring Boot Actuator等监控组件实时观察线程池的队列堆积、活跃线程数等指标提前发现资源瓶颈。并发编程的学习曲线陡峭但回报丰厚。它不仅能帮你解决棘手的线上问题更能从根本上提升你对计算机程序执行、操作系统调度、内存模型等底层原理的理解。从理解volatile和synchronized的语义开始到熟练运用JUC工具包再到能设计出无锁的数据结构每一步都需要大量的思考和实践。这份笔记只是一个起点真正的掌握来自于在项目中的不断试错、复盘和优化。记住在并发世界里谨慎和清晰的设计永远比炫技更重要。当你对每一行可能被多线程访问的代码都抱有敬畏之心时你就离一名优秀的并发程序员不远了。