1. 项目概述从“锁”开始聊聊Java并发编程的基石在Java的世界里只要你开始接触多线程编程“锁”这个概念就会像影子一样跟着你。无论是处理高并发的Web服务还是优化一个后台批处理任务如何安全、高效地协调多个线程对共享资源的访问是每个Java开发者必须跨过的坎。我自己在早期做项目时就曾因为对锁的理解不够深入导致过一个线上服务在流量稍大时就出现数据错乱排查了整整一个通宵才找到是同步块用错了地方。所以今天我想抛开那些晦涩的教科书定义结合我踩过的坑和积累的经验和大家深入聊聊Java中最核心、最常用的三种锁机制synchronized、ReentrantLock和ReentrantReadWriteLock。简单来说你可以把它们理解成管理一个热门会议室的不同策略。synchronized是最简单的“一把钥匙开一把锁”谁拿到钥匙谁进去别人只能干等着ReentrantLock则像是一个更智能的门禁系统可以查看排队情况、设置等待时间甚至公平地安排入场顺序而ReentrantReadWriteLock则进行了更精细的区分它允许很多人同时进去“读”比如查阅资料但一旦有人要“写”比如修改资料就必须清场独占。理解这三者的区别、适用场景以及背后的“为什么”是写出健壮、高效并发代码的关键。无论你是正在准备面试被各种“锁”的八股文困扰还是在实际开发中遇到了性能瓶颈或诡异的线程安全问题这篇文章都将从原理到实战给你一份清晰的指南。2. 核心锁机制深度解析与设计哲学在深入每个锁的实现之前我们必须先建立几个核心认知。并发编程的核心矛盾在于速度与正确性的权衡。不加控制的多线程访问能最大化利用CPU资源速度但极易导致数据不一致正确性丢失。锁就是引入一定的串行化牺牲一点速度来换取操作的正确性。但不同的锁其设计哲学和牺牲的“代价”侧重点不同。2.1 synchronizedJVM内置的“元老级”锁synchronized是Java语言层面的关键字是JVM原生支持的互斥同步机制。它的设计哲学是简单、自动与可靠。你不需要显式地创建锁对象、加锁、解锁JVM帮你打理好了一切。底层原理与锁升级这是理解synchronized性能的关键。很多人认为synchronized很重那是老黄历了。现代JVMHotSpot为了减少获得锁和释放锁带来的性能开销引入了“偏向锁”、“轻量级锁”、“重量级锁”的锁状态升级过程。无锁初始状态。偏向锁假设大多数情况下锁不存在竞争总是由同一线程多次获得。当一个线程访问同步块时会在对象头和栈帧中的锁记录里存储偏向的线程ID以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁只需简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。这能消除无竞争下的同步开销。轻量级锁当有另一个线程来竞争锁时偏向锁就会升级为轻量级锁。线程会在自己的栈帧中创建锁记录Lock Record然后通过CAS操作尝试将对象头的Mark Word更新为指向锁记录的指针。如果成功当前线程获得锁如果失败表示有竞争会自旋尝试获取锁自适应自旋。重量级锁如果轻量级锁的竞争依然激烈比如自旋超过一定次数就会膨胀为重量级锁。此时未获得锁的线程会被阻塞进入等待队列依赖于操作系统底层的互斥量Mutex来实现涉及到用户态到内核态的切换开销最大。这个升级过程是自适应且不可逆的批量重偏向等特殊情况除外。它的核心思想是在无竞争或低竞争时用最小的开销偏向、轻量级锁在高竞争时果断切换到最稳定的方案重量级锁避免无意义的自旋消耗CPU。可重入性synchronized是可重入的。这意味着同一个线程在已经持有锁的情况下可以再次进入被同一个锁保护的同步块或方法。JVM通过为每个锁关联一个持有计数器_recursions和一个持有线程来实现。每次重入计数器加1退出时计数器减1计数器为0时锁才被真正释放。这避免了线程自己把自己锁死的“单线程死锁”。使用方式修饰实例方法锁是当前实例对象this。public synchronized void instanceMethod() { // 同步代码 }修饰静态方法锁是当前类的Class对象MyClass.class。public static synchronized void staticMethod() { // 同步代码 }修饰代码块锁是synchronized括号里配置的对象。public void method() { synchronized (lockObject) { // 同步代码块 } }注意选择锁对象至关重要。错误地使用字符串字面量或包装类对象如Integer作为锁可能因为JVM的常量池或缓存机制导致意外的锁竞争引发严重问题。最佳实践是使用一个私有的、final的Object实例作为专用锁private final Object lock new Object();。2.2 ReentrantLock灵活强大的“手动挡”锁ReentrantLock是java.util.concurrent.locks包下的一个类它在Lock接口的基础上实现。它的设计哲学是灵活、可控与功能丰富。如果说synchronized是自动挡汽车那ReentrantLock就是手动挡给了司机开发者更多的控制权但同时也要求司机更懂行。核心特性对比可中断的锁获取lockInterruptibly()方法允许在等待锁的过程中响应中断。这对于实现可取消的任务非常重要。超时锁获取tryLock(long time, TimeUnit unit)方法可以尝试在指定时间内获取锁超时则返回false。这是避免死锁和构建高响应系统的重要工具。公平性选择构造函数可以传入一个boolean值决定是否创建公平锁。公平锁保证等待时间最长的线程优先获得锁避免了“饥饿”现象但会带来更大的性能开销需要维护有序队列。非公平锁是默认的也是性能更高的选择因为它允许“插队”减少了线程挂起和唤醒的开销。条件变量Condition一个ReentrantLock可以关联多个Condition对象用于实现更精细的线程等待/通知机制。相比Object的wait()/notify()Condition的await()/signal()可以精确地唤醒特定类型的等待线程避免了“惊群效应”。底层实现ReentrantLock的核心是基于AQSAbstractQueuedSynchronizer抽象队列同步器。AQS内部维护了一个volatile的state变量表示锁状态和一个CLHCraig, Landin, and Hagersten变体的FIFO双向队列管理等待线程。加锁解锁的本质就是通过CAS操作修改state成功则获取锁失败则通过队列管理线程的阻塞与唤醒。公平与非公平的实现差异主要体现在新来的线程是否要先检查队列中是否有等待者。基本使用模板Lock lock new ReentrantLock(); // ... 或者使用公平锁Lock lock new ReentrantLock(true); lock.lock(); // 最好放在try块外确保锁一定被获取 try { // 访问共享资源 } finally { lock.unlock(); // 确保锁一定被释放 }这个try-finally块是使用ReentrantLock的黄金法则必须严格遵守以防异常导致锁无法释放。2.3 ReentrantReadWriteLock读写分离的“高效锁”ReentrantReadWriteLock同样是java.util.concurrent.locks包下的类它实现了ReadWriteLock接口。它的设计哲学是区分操作类型提升并发度。对于“读多写少”的场景它允许同时有多个读线程但写线程独占。这极大地提高了系统的吞吐量。核心思想将锁拆分为一个读锁ReadLock和一个写锁WriteLock。其规则可以概括为读读共享多个线程可以同时持有读锁。读写互斥如果一个线程持有写锁其他线程不能获得读锁或写锁如果一个线程持有读锁其他线程不能获得写锁。写写互斥同一时间只能有一个线程持有写锁。锁降级这是ReentrantReadWriteLock一个非常重要且容易被忽略的特性。它允许一个已经持有写锁的线程在保持写锁的同时获取读锁然后释放写锁从而“降级”为读锁。这个过程是允许的因为持有写锁的线程本身就是数据的唯一修改者它获取读锁是安全的。锁降级的主要目的是保证数据的可见性。在释放写锁后其他写线程可能立即修改数据如果此时原线程没有读锁它后续的读操作可能读到旧数据。持有读锁可以防止其他写线程介入确保自己能看到自己刚修改完的数据。ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); ReentrantReadWriteLock.WriteLock writeLock rwLock.writeLock(); ReentrantReadWriteLock.ReadLock readLock rwLock.readLock(); writeLock.lock(); try { // 修改数据... // 获取读锁锁降级开始 readLock.lock(); } finally { writeLock.unlock(); // 释放写锁降级为读锁 } try { // 仍然持有读锁可以安全地读取数据其他读线程也可以并发读 // 但其他写线程被阻塞 } finally { readLock.unlock(); }注意ReentrantReadWriteLock不支持锁升级即持有读锁时尝试获取写锁。这会导致死锁因为读锁未释放时写锁永远无法获取读写互斥而当前线程又在等待写锁从而自己把自己卡死。潜在问题——写线程饥饿在极端读多写少的场景下如果读锁被持续持有写线程可能会长时间得不到执行机会导致“饥饿”。ReentrantReadWriteLock的公平模式可以在一定程度上缓解这个问题但代价是性能下降。对于这种场景Java 8引入了StampedLock它通过乐观读提供了另一种解决方案性能通常更高。3. 三种锁的对比分析与选型指南了解了各自的特点后我们如何在实际项目中选择下面这个表格从多个维度进行了对比特性维度synchronized(关键字)ReentrantLock(类)ReentrantReadWriteLock(类)锁的获取JVM隐式获取/释放自动管理需显式调用lock()/unlock()必须在finally中释放需显式获取读锁readLock()或写锁writeLock()可中断不支持。等待锁的线程不可被中断支持。lockInterruptibly()读锁和写锁的获取都支持可中断模式超时尝试不支持支持。tryLock(timeout)读锁和写锁都支持tryLock(timeout)公平性不支持公平锁支持构造函数传入true默认非公平支持构造函数传入true默认非公平条件队列单一。通过Object.wait()/notify()多个。可创建多个Condition对象写锁可以创建Condition读锁不能可重入性支持支持读锁和写锁分别支持可重入且写锁可重入为读锁降级性能JDK6后优化很好在低竞争下性能佳在高竞争下性能可能优于synchronized读多写少场景下并发度远高于互斥锁锁降级不涉及不涉及支持是重要特性锁升级不涉及不涉及不支持会导致死锁编码复杂度低语法简单中需注意异常处理和锁释放中高需清晰区分读写场景选型决策树与实战心得首选synchronized如果你的同步需求非常简单不需要中断、超时、公平锁、多个条件变量等高级功能并且竞争强度不高无脑用synchronized。它的代码更简洁由JVM负责优化和释放不易出错。这是Java并发大师Brian Goetz的建议“除非你需要ReentrantLock的高级功能否则请优先使用synchronized。”考虑ReentrantLock当你的场景需要以下任何一个特性时可中断的锁等待比如实现一个可以随时取消的任务。尝试获取锁带超时比如在死锁检测、构建高可用服务时避免线程无限期等待。公平锁需要严格的先来后到顺序注意性能代价。多个等待条件典型的生产者-消费者模型可以用两个Condition分别管理队列空和队列满的等待线程比wait/notifyAll更高效精准。考虑ReentrantReadWriteLock当你的数据结构明显是读操作频率远高于写操作例如配置信息缓存、元数据查询并且读操作本身不修改数据时。它能带来数量级级别的吞吐量提升。但务必注意写线程饥饿问题并善用锁降级保证数据一致性。更高级的选择在Java 8的极高并发读场景下可以研究StampedLock的乐观读模式。对于简单的计数器Atomic原子变量可能是无锁的更好选择。一个常见的误区认为ReentrantLock一定比synchronized快。这在JDK5早期可能是成立的但随着JVM对synchronized的持续优化如偏向锁、轻量级锁、锁消除、锁粗化在大多数常规竞争场景下两者的性能差距已经微乎其微甚至synchronized可能更优因为它有更多的优化空间。选择ReentrantLock应该是基于其功能特性而非单纯的性能假设。4. 实战示例与避坑指南理论说再多不如看代码。下面我们通过几个典型的场景来演示这三种锁的具体用法并附上我踩过的坑和总结的经验。4.1 场景一简单的线程安全计数器这是一个最基础的场景我们用三种方式来实现。使用synchronizedpublic class SynchronizedCounter { private int count 0; // 使用synchronized修饰方法锁是当前实例对象 public synchronized void increment() { count; // count不是原子操作需要同步 } public synchronized int getCount() { return count; } }心得对于这种简单的getter/setter同步synchronized方法是最清晰直接的。但要注意如果getCount()调用非常频繁而写操作很少这种互斥锁会成为瓶颈。此时可以考虑使用volatile原子操作或者后面提到的读写锁。使用ReentrantLockimport java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockCounter { private int count 0; private final Lock lock new ReentrantLock(); // 创建锁对象 public void increment() { lock.lock(); // 手动加锁 try { count; } finally { lock.unlock(); // 必须放在finally块确保释放 } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } } }避坑指南黄金法则lock()调用必须在try块之外。如果放在try内部并且在lock()成功前发生异常虽然罕见会导致finally块执行unlock()而实际上锁并未被当前线程持有从而抛出IllegalMonitorStateException。解锁务必在finally这是防止异常导致锁泄漏死锁的生命线。我曾见过在increment()方法里直接lock.lock(); count; lock.unlock();一旦count抛出异常比如数组越界等锁就永远无法释放。使用ReentrantReadWriteLockimport java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockCounter { private int count 0; private final ReadWriteLock rwLock new ReentrantReadWriteLock(); public void increment() { rwLock.writeLock().lock(); // 写操作需要获取写锁 try { count; } finally { rwLock.writeLock().unlock(); } } public int getCount() { rwLock.readLock().lock(); // 读操作获取读锁允许多线程并发 try { return count; } finally { rwLock.readLock().unlock(); } } }适用性分析在这个简单的计数器例子中使用读写锁可能有点“杀鸡用牛刀”。因为getCount()只是读一个int本身是原子性的在x86架构下甚至不需要同步但为了Java内存模型的可见性仍需处理。读写锁的优势在于读操作本身很重比如读一个大的HashMap并进行计算的场景。这里主要是展示模式。4.2 场景二生产者-消费者模型使用Condition这是一个经典的多线程协作问题ReentrantLock配合Condition能实现得非常优雅。import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ProducerConsumerWithCondition { private final QueueInteger queue new LinkedList(); private final int maxSize 10; private final Lock lock new ReentrantLock(); private final Condition notFull lock.newCondition(); // 队列未满条件 private final Condition notEmpty lock.newCondition(); // 队列非空条件 public void produce(int value) throws InterruptedException { lock.lock(); try { while (queue.size() maxSize) { // 必须用while防止虚假唤醒 System.out.println(队列已满生产者等待...); notFull.await(); // 在notFull条件上等待 } queue.offer(value); System.out.println(生产: value 队列大小: queue.size()); notEmpty.signalAll(); // 生产了一个通知可能正在等待的消费者 } finally { lock.unlock(); } } public Integer consume() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { // 必须用while System.out.println(队列为空消费者等待...); notEmpty.await(); // 在notEmpty条件上等待 } Integer value queue.poll(); System.out.println(消费: value 队列大小: queue.size()); notFull.signalAll(); // 消费了一个通知可能正在等待的生产者 return value; } finally { lock.unlock(); } } }核心要点与避坑while循环检查条件这是绝对必须的。Object.wait()和Condition.await()都可能因为底层操作系统的原因发生“虚假唤醒”即线程在没有被notify或signal的情况下醒来。用while循环重新检查条件可以保证即使被虚假唤醒如果条件不满足线程会继续等待。精准通知使用两个ConditionnotFull和notEmpty可以分别管理因队列满而等待的生产者和因队列空而等待的消费者。当生产者生产一个元素后它只需要signal或signalAll在notEmpty上等待的消费者而不会错误地唤醒其他生产者。这比使用synchronized时只能用notifyAll唤醒所有线程要高效得多减少了不必要的上下文切换。signal()vssignalAll()signal()只唤醒一个等待线程效率更高但需要确保被唤醒的线程是“正确”的比如唤醒的是消费者而不是生产者。在简单的生产者-消费者模型中由于条件队列是分开的用signal()是安全的。在更复杂的场景下如果无法确定唤醒谁是正确的使用signalAll()更稳妥但性能有损耗。4.3 场景三缓存实现与锁降级实战这是一个读写锁的经典应用场景一个内存缓存读多写少且写操作刷新缓存成本高。import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class SimpleCacheK, V { private final MapK, V cache new HashMap(); private final ReadWriteLock rwLock new ReentrantReadWriteLock(); // 读缓存使用读锁允许多线程并发 public V get(K key) { rwLock.readLock().lock(); try { return cache.get(key); } finally { rwLock.readLock().unlock(); } } // 写缓存使用写锁独占 public void put(K key, V value) { rwLock.writeLock().lock(); try { cache.put(key, value); } finally { rwLock.writeLock().unlock(); } } // 一个更复杂但常见的操作如果缓存不存在则加载懒加载 public V getWithLoad(K key, DataLoaderK, V loader) { V value; // 第一步先尝试用读锁读 rwLock.readLock().lock(); try { value cache.get(key); if (value ! null) { return value; // 缓存命中直接返回 } } finally { rwLock.readLock().unlock(); // 释放读锁准备获取写锁 } // 第二步缓存未命中需要加载。获取写锁此时可能有其他线程也在竞争写锁准备加载同一个key rwLock.writeLock().lock(); try { // 再次检查Double-Check因为可能当前线程获取写锁的过程中其他线程已经加载完成了 value cache.get(key); if (value null) { // 执行昂贵的加载操作 value loader.load(key); cache.put(key, value); } // 锁降级在此处发生在持有写锁的情况下获取读锁 rwLock.readLock().lock(); } finally { rwLock.writeLock().unlock(); // 释放写锁此时仍持有读锁 } // 第三步在持有读锁的情况下使用并返回数据 try { return value; } finally { rwLock.readLock().unlock(); // 最后释放读锁 } } interface DataLoaderK, V { V load(K key); } }深度解析与避坑Double-Check的必要性在getWithLoad方法中我们在获取写锁后再次检查了缓存。这是因为在“释放读锁”和“获取写锁”这两个操作之间有一个时间窗口可能有多个线程同时发现缓存缺失都释放了读锁去竞争写锁。第一个获得写锁的线程加载数据后后续获得写锁的线程不应该重复加载。这个Double-Check是避免重复加载、保证性能的关键。锁降级的精妙之处在写锁块内获取读锁然后释放写锁这就是锁降级。为什么要这么做考虑一下如果不降级直接释放写锁然后返回value。在释放写锁的瞬间另一个写线程可能立即获取写锁并修改了key对应的值甚至删除了这个条目那么当前线程返回的value可能已经过时或无效。通过降级为读锁我们保证了在返回数据给调用者的整个读操作期间其他写线程无法修改这个数据从而保证了数据一致性视图。这是读写锁场景下保证线程安全的一个高级技巧。性能权衡这个getWithLoad方法在缓存命中时性能很好只有一次读锁开销。在缓存未命中时虽然经历了读锁-写锁-读锁的转换开销较大但保证了数据的正确加载和一致性。对于加载成本极高的操作如数据库查询、网络调用这个开销是值得的。如果加载成本很低或许用互斥锁synchronized或ReentrantLock实现一个更简单的方法可能更合适。5. 高级话题、性能调优与面试精要掌握了基本使用和常见场景后我们还需要关注一些更深层次的问题和优化技巧这些也是面试中的高频考点。5.1 死锁的预防、诊断与解决死锁是并发编程的噩梦。它发生在两个或多个线程互相持有对方所需的资源并无限期地等待对方释放。产生死锁的四个必要条件柯恩条件互斥、持有并等待、不可剥夺、循环等待。示例一个简单的死锁Object lockA new Object(); Object lockB new Object(); Thread t1 new Thread(() - { synchronized (lockA) { try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (lockB) { // 等待t2释放lockB System.out.println(Thread 1 got both locks); } } }); Thread t2 new Thread(() - { synchronized (lockB) { try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (lockA) { // 等待t1释放lockA System.out.println(Thread 2 got both locks); } } });预防策略固定顺序获取锁强制所有线程以相同的全局顺序获取锁。例如规定必须先获取lockA再获取lockB。这样t2的代码就需要调整。超时放弃使用ReentrantLock.tryLock(long timeout, TimeUnit unit)。尝试获取锁失败后释放已持有的锁等待一段时间重试。这需要业务逻辑支持回滚。避免嵌套锁尽量降低锁的粒度减少需要同时持有多个锁的场景。如果必须仔细设计获取顺序。使用开放调用在调用外部方法时不持有锁。这能有效打破“持有并等待”的条件。诊断工具jstack最常用的命令行工具。jstack pid可以打印出Java进程的所有线程栈。死锁线程会显示BLOCKED状态并在最后有明确的“Found one Java-level deadlock”提示指出哪些线程在等待哪些锁。JConsole/VisualVM图形化工具线程选项卡可以检测死锁。编程式检测ThreadMXBean可以编程式查找死锁。ThreadMXBean bean ManagementFactory.getThreadMXBean(); long[] threadIds bean.findDeadlockedThreads(); if (threadIds ! null) { // 处理死锁... }5.2 性能调优与最佳实践减小锁粒度锁细化不要用一个大锁保护整个复杂对象而是用多个锁保护对象内部不同的独立数据。例如ConcurrentHashMap内部使用了分段锁JDK7或CASsynchronizedJDK8比用synchronized包装整个HashMap性能高得多。缩短锁持有时间只在必须同步的代码块上加锁。尽可能把不需要同步的预处理、后处理逻辑移到锁外。我见过最典型的反例是在同步块内进行IO操作如日志写入、网络请求这会让其他线程长时间阻塞。锁粗化这通常是JVM自动进行的优化。如果虚拟机探测到有一串零碎的操作都对同一个对象反复加锁解锁会把加锁同步的范围扩展粗化到整个操作序列的外部从而减少不必要的锁操作开销。作为开发者我们应避免人为地写出需要JVM进行锁粗化的代码比如在循环内加锁而应主动将锁提到循环外。使用无锁编程对于简单的状态更新优先考虑java.util.concurrent.atomic包下的原子类如AtomicInteger,LongAdder。LongAdder在高并发统计场景下比AtomicLong性能更好它采用了分段累加的思想。ConcurrentHashMap在JDK8中也大量使用了CAS无锁算法。读写锁的选择再次强调只有在读操作远大于写操作且读操作本身有一定开销时使用ReentrantReadWriteLock才能带来正收益。如果读操作非常快比如读一个volatile变量读写锁本身的维护开销可能会抵消其带来的并发度优势。考虑StampedLock在Java 8中引入它提供了三种模式的锁写锁、悲观读锁和乐观读。乐观读不阻塞写锁性能极高但需要额外的代码来验证在读过程中数据是否被修改通过validate(stamp)方法。适用于读非常多、写非常少且对数据一致性要求不是极端严格的场景。5.3 高频面试题深度剖析synchronized和ReentrantLock的区别本质synchronized是JVM关键字ReentrantLock是JDK API。功能ReentrantLock更灵活支持可中断、超时、公平锁、多条件变量。性能JDK6后两者在大多数场景下性能接近。选型基于功能需求而非性能。释放synchronized自动释放ReentrantLock必须手动unlock()通常在finally中。底层synchronized依赖JVM的锁升级ReentrantLock基于AQS。什么是可重入锁为什么需要它定义同一个线程可以多次获取同一把锁而不会产生死锁。实现JVM或AQS通过维护一个持有计数器_recursions或state的高位和持有线程ID来实现。必要性在面向对象编程中一个同步方法很可能调用另一个同步方法甚至是父类的或者递归调用自身。如果锁不可重入线程会在调用自己的时候被自己阻塞导致“单线程死锁”。ReentrantReadWriteLock的锁降级和锁升级锁降级支持。持有写锁 - 获取读锁 - 释放写锁。目的是保证数据可见性防止在释放写锁后、本线程读取数据前数据被其他写线程修改。锁升级不支持。持有读锁 - 尝试获取写锁。这会导致死锁因为读锁未释放时写锁永远无法获取读写互斥而当前线程又在等待写锁。AQSAbstractQueuedSynchronizer是什么定位是ReentrantLock、CountDownLatch、Semaphore等并发工具类的底层同步框架。核心一个volatile的int状态变量state 一个FIFO线程等待队列CLH变体。原理子类通过CAS操作修改state来定义“获取锁”和“释放锁”的逻辑。获取失败则通过LockSupport.park()将线程加入队列并阻塞释放锁时唤醒队列中的后继节点。模板方法模式AQS定义了排队和阻塞/唤醒的骨架子类只需实现tryAcquire、tryRelease等保护方法来定义具体的同步语义。synchronized的锁优化有哪些锁升级无锁 - 偏向锁 - 轻量级锁 - 重量级锁。锁消除JIT编译器通过逃逸分析发现某些锁对象不可能被其他线程访问就会将这些锁消除。锁粗化将连续的对同一锁的请求合并扩大锁的范围减少频繁的加锁解锁操作。自适应自旋线程在获取轻量级锁失败时不会立即阻塞而是自旋等待。JVM会根据以往自旋的成功历史动态调整自旋时间。理解并掌握这些锁机制不仅仅是应付面试更是构建高并发、高可靠Java应用的基石。在实际编码中时刻保持对线程安全的警惕优先使用更高级的并发工具如ConcurrentHashMap,CopyOnWriteArrayList在必须使用锁时仔细评估场景选择最合适的那一把“钥匙”。记住没有最好的锁只有最合适的锁。