
1. 从“锁”字说起并发编程的基石与日常误解最近在面试和带新人的过程中我发现一个挺有意思的现象很多朋友一提到Java并发脑子里蹦出来的第一个词就是“锁”。无论是准备面试八股文还是处理线上数据库死锁、分布式锁超时甚至是电脑锁屏密码失效这种看似不相关的问题“锁”这个概念都无处不在。但大家真的理解“锁”在并发编程里到底扮演什么角色吗或者说我们是不是把“锁”想得太简单了举个例子你可能会在网上搜“乐观锁悲观锁区别”来应付面试但当你真正在代码里用synchronized或者ReentrantLock时有没有想过为什么这段代码加了锁就慢了那个java.lang.NullPointerException的映射处理器内部错误会不会和锁的持有状态有关又或者当你配置Transactional注解时数据库的事务隔离级别和你代码里的synchronized锁到底谁先谁后会不会互相打架今天我们不聊那些干巴巴的概念罗列。我想从一个一线开发者的视角带你重新梳理一遍Java里的“锁”。我们不止要搞清楚synchronized、ReentrantLock、ReadWriteLock这些家伙怎么用更要挖一挖它们背后的“小心思”JVM和操作系统是怎么协作实现锁的为什么会有“锁膨胀”“无锁”编程是不是真的更快以及当我们在谈论数据库的“间隙锁”、“记录锁”时和Java内存模型里的“锁”到底是不是一回事这篇文章会很长超过五千字但我会尽量用大白话和实际场景把这块硬骨头啃下来。无论你是正在被“Java并发编程”困扰的初学者还是想深入理解锁机制来优化系统性能的老手相信都能找到一些对你有用的东西。我们从一个最简单的场景开始。2. 锁的本质协调多线程对共享资源的访问要理解锁我们得先回到问题的源头为什么需要锁想象一下你和几个同事共用一台打印机。如果大家同时点击“打印”结果就是一堆文件杂乱地挤在一起可能还夹杂着半张纸。这里的“打印机”就是“共享资源”而“大家同时操作”就是“并发访问”。锁的作用就是像打印机队列一样让大家的打印任务排队一个一个来保证每个任务都能完整、正确地执行。在Java程序里这个“共享资源”通常就是堆内存中的对象实例、静态变量或者一个文件句柄。当多个线程同时读写同一块内存区域时如果没有协调机制就会发生竞态条件。最经典的例子就是计数器public class Counter { private int count 0; public void add() { count; // 这行代码是“祸根” } }count这行代码看起来是原子操作但实际上它至少包含三个步骤1. 读取当前count值到线程工作内存2. 将值加13. 将新值写回主内存。如果两个线程A和B同时执行add()它们可能都读到同样的初始值比如0各自加1后都写回1最终结果应该是2但实际上却是1。这就是并发编程中最经典的“丢失更新”问题。锁就是为了解决这个问题而生的。它的核心职责是提供互斥和可见性保证。互斥同一时刻只允许一个线程持有锁并执行临界区代码比如count这段。这解决了竞态条件。可见性当一个线程释放锁时它对共享变量所做的修改必须对后续获得锁的线程可见。这解决了因为CPU缓存导致的数据不一致问题。Java语言层面和JVM为我们提供了多种锁的实现从最基础的synchronized关键字到功能更丰富的java.util.concurrent.locks包下的各种锁工具。但在这之前我们必须先理解一个更底层的概念Java内存模型。2.1 锁与Java内存模型的关系很多人学并发直接跳进去学synchronized和volatile的语法却忽略了它们存在的基石——Java内存模型。JMM定义了线程如何以及何时可以看到其他线程修改过的共享变量以及如何同步地访问共享变量。JMM规定所有的变量都存储在主内存中每个线程还有自己的本地内存可以粗略理解为CPU高速缓存线程对变量的所有操作都必须在本地内存中进行不能直接读写主内存。这就带来了问题线程A修改了本地内存中的变量线程B可能根本不知道。synchronized和volatile关键字就是Java提供给我们的“同步原语”它们会在操作前后插入特定的内存屏障强制线程将本地内存的修改刷新到主内存或者从主内存重新读取变量值。一个关键的理解synchronized锁住的不仅仅是一段代码的执行顺序更是一段符合JMM规范的内存访问协议。当线程进入synchronized块时它相当于执行了一次read和load操作从主内存读取共享变量的最新值。当线程退出synchronized块时它会执行store和write操作把修改刷回主内存。这个“进入-退出”的过程天然保证了临界区内操作的可见性和有序性。所以当你下次看到synchronized不应该只想到“排队”还应该想到“内存可见性保证”。这也是为什么有些情况下即使代码逻辑看起来没有竞态条件我们仍然需要加锁——就是为了保证其他线程能立刻看到修改。3. 内置锁深入 synchronized 的“黑盒”synchronized是Java最原始、最常用的锁机制。它的用法很简单修饰实例方法、静态方法或者同步代码块。// 1. 同步实例方法锁是当前对象实例(this) public synchronized void instanceMethod() { // ... } // 2. 同步静态方法锁是当前类的Class对象 public static synchronized void staticMethod() { // ... } // 3. 同步代码块需指定锁对象 public void someMethod() { synchronized (lockObject) { // 临界区 } }语法虽然简单但synchronized背后的实现却非常精妙而且一直在进化。理解这个进化过程对你写出高性能并发代码至关重要。3.1 从重量级锁到自适应自旋锁的升级与优化在早期Java版本中synchronized的实现直接依赖于操作系统的互斥量。线程获取锁失败就会被操作系统挂起进入内核态发生线程上下文切换。这个开销非常大所以被称为“重量级锁”。后来JVM团队引入了“偏向锁”和“轻量级锁”等优化核心思想是大多数情况下锁不仅不存在多线程竞争而且总是由同一个线程多次获得。既然如此为什么每次都要走那么重的流程呢于是synchronized的锁状态变得复杂起来它有一个升级路径无锁状态一个新对象默认处于无锁状态。偏向锁当第一个线程来访问同步块时JVM会将对象头中的标记设置为“偏向模式”并记录下这个线程的ID。以后这个线程再进入和退出同步块时不需要进行任何同步操作如CAS、锁释放直接检查线程ID即可开销极小。这适用于“锁总是被同一个线程使用”的场景。轻量级锁当有第二个线程来尝试获取锁时发生了竞争偏向锁就会升级为轻量级锁。轻量级锁的实现基于“自旋”和CAS操作。线程不会立即被挂起而是在一个循环里不断尝试获取锁自旋。如果很快就能获取到比如持有锁的线程很快就释放了那么就能避免昂贵的线程挂起和唤醒操作。重量级锁如果轻量级锁的竞争依然激烈自旋了一定次数后JDK 6之后是自适应自旋JVM会根据上次自旋成功与否动态调整自旋时间还没拿到锁锁就会膨胀为重量级锁。此时未获取到锁的线程会被挂起进入阻塞队列等待被唤醒。为什么需要了解这个因为在不同的竞争场景下锁的表现天差地别。如果你写的是一个热点方法被成千上万个线程频繁调用那么大部分线程可能都会在轻量级锁阶段自旋消耗大量CPU资源最终仍然升级为重量级锁性能暴跌。这时候你可能就需要考虑减少锁的粒度或者换用并发容器、无锁算法了。实操心得如何观察锁状态虽然我们无法在运行时直接修改锁状态但可以通过工具来观察。使用jstack命令 dump 线程栈可以看到线程是BLOCKED状态等待重量级锁还是RUNNABLE状态可能在自旋。更专业的工具如JProfiler、Async-Profiler可以直观地显示锁竞争的热点和等待时间。记住一个原则如果jstack日志里大量线程阻塞在同一个锁对象上这就是一个强烈的性能警告信号。3.2 synchronized 的局限性synchronized很好用但它是个“霸道总裁”功能比较单一不可中断线程在等待synchronized锁时无法被中断只能一直等下去。非公平锁synchronized的锁获取默认是非公平的。这意味着一个刚被唤醒的线程和一个刚刚来尝试获取锁的线程它们竞争的机会是不公平的新来的线程可能直接插队成功。在高并发下这可能导致某些线程“饥饿”永远拿不到锁。单一条件一个synchronized锁只关联一个隐式的等待队列wait/notify。如果你想实现“生产者-消费者”模型当缓冲区满时生产者等待空时消费者等待用单一的wait/notify就比较麻烦容易出错。正因为这些局限性在更复杂的并发场景下我们就需要请出java.util.concurrent.locks包里的“瑞士军刀”了。4. 显式锁ReentrantLock 的精细控制ReentrantLock是synchronized的增强版它提供了更灵活、更强大的功能。基本用法如下Lock lock new ReentrantLock(); try { lock.lock(); // 手动加锁 // 临界区代码 } finally { lock.unlock(); // 必须在finally块中手动释放锁 }看起来比synchronized麻烦是的但它带来的能力提升是值得的。4.1 核心特性剖析1. 可中断的锁获取lock.lockInterruptibly()方法允许在等待锁的过程中响应中断。这对于实现可取消的任务非常重要。public void interruptibleTask() throws InterruptedException { Lock lock new ReentrantLock(); try { lock.lockInterruptibly(); // 这里可以响应Thread.interrupt() // ... 执行任务 } finally { lock.unlock(); } }想象一个场景用户发起一个需要获取数据库连接的操作这个操作需要竞争一个锁。如果用户中途取消了请求你可以中断这个等待锁的线程而不是让它傻等到天荒地老。2. 尝试非阻塞获取锁tryLock()方法尝试获取锁如果锁可用则立即返回true否则立即返回false。这避免了线程的无谓等待。if (lock.tryLock()) { try { // 获取锁成功处理共享资源 } finally { lock.unlock(); } } else { // 获取锁失败执行备选方案比如记录日志、重试、或者直接返回错误 }tryLock(long time, TimeUnit unit)还可以带超时时间在指定时间内尝试获取锁。3. 公平锁与非公平锁创建ReentrantLock时可以传入一个boolean参数决定是否公平。Lock fairLock new ReentrantLock(true); // 公平锁 Lock unfairLock new ReentrantLock(); // 或 new ReentrantLock(false) 非公平锁公平锁严格按照线程请求锁的顺序FIFO来分配锁。优点是避免饥饿缺点是整体吞吐量较低因为维护队列有开销。非公平锁允许“插队”。当一个线程释放锁时如果正好有另一个线程在尝试获取锁那么这个新线程可能直接抢到锁而不用去队列末尾排队。优点是吞吐量高缺点是可能导致某些线程长时间饥饿。如何选择除非你对公平性有严格的要求比如防止低优先级线程饿死否则默认使用非公平锁。在大多数高并发场景下非公平锁能减少线程切换提供更高的吞吐量。synchronized就是非公平的。4. 绑定多个条件一个ReentrantLock可以创建多个Condition对象实现更精细的线程等待/通知机制。class BoundedBuffer { final Lock lock new ReentrantLock(); final Condition notFull lock.newCondition(); // 条件不满 final Condition notEmpty lock.newCondition(); // 条件不空 final Object[] items new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count items.length) // 缓冲区满 notFull.await(); // 在“不满”条件上等待 items[putptr] x; if (putptr items.length) putptr 0; count; notEmpty.signal(); // 唤醒一个在“不空”条件上等待的线程消费者 } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count 0) // 缓冲区空 notEmpty.await(); // 在“不空”条件上等待 Object x items[takeptr]; if (takeptr items.length) takeptr 0; --count; notFull.signal(); // 唤醒一个在“不满”条件上等待的线程生产者 return x; } finally { lock.unlock(); } } }这个“生产者-消费者”模型的实现比用synchronized配合单个wait/notifyAll要清晰、高效得多因为notifyAll会唤醒所有等待线程而Condition.signal()通常只唤醒一个减少了不必要的竞争。4.2 显式锁的“坑”与最佳实践能力越大责任越大。ReentrantLock需要手动释放锁这就带来了新的风险。1. 忘记在 finally 中解锁这是最常见的错误。如果在临界区代码中抛出了异常而锁没有在finally块中释放那么这个锁将永远无法被释放导致所有其他线程永久等待死锁的一种形式。务必务必务必将unlock()放在finally块中2. 锁的粒度问题无论是synchronized还是ReentrantLock锁的粒度过粗都会严重限制并发度。例如锁住整个服务类不如锁住一个具体的资源对象。在设计时要尽量缩小临界区的范围只锁住必须共享的数据。3. 死锁这是并发编程的经典难题。死锁的四个必要条件互斥、持有并等待、不可剥夺、循环等待。避免死锁的常见策略顺序加锁所有线程都按照固定的全局顺序来申请锁。比如有锁A和锁B规定必须先申请A再申请B。超时放弃使用tryLock(long time, TimeUnit unit)获取锁失败超时后释放自己已持有的锁然后重试。ReentrantLock对这个场景的支持比synchronized好得多。死锁检测对于复杂系统可以定期扫描线程和锁的依赖图发现环路则报警或采取强制措施如中断某个线程。5. 读写锁与乐观锁应对不同的并发场景不是所有的共享资源访问都需要“互斥”。读多写少的场景非常普遍比如缓存系统、配置信息。如果只是读操作多个线程同时进行是安全的完全不需要阻塞。ReadWriteLock就是为这种场景设计的。5.1 ReadWriteLock读共享写互斥ReentrantReadWriteLock是它的一个实现。它维护了一对锁一个读锁和一个写锁。读锁是共享锁。只要没有线程持有写锁任意多个线程都可以同时持有读锁。写锁是独占锁。一旦有线程持有写锁其他任何线程无论是读还是写都无法获取锁。public class Cache { private final MapString, Object map new HashMap(); private final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); private final Lock readLock rwLock.readLock(); private final Lock writeLock rwLock.writeLock(); public Object get(String key) { readLock.lock(); try { return map.get(key); } finally { readLock.unlock(); } } public void put(String key, Object value) { writeLock.lock(); try { map.put(key, value); } finally { writeLock.unlock(); } } }使用注意事项锁降级ReentrantReadWriteLock支持锁降级即先获取写锁再获取读锁然后释放写锁。这样当前线程仍然持有读锁可以防止其他写线程的干扰同时允许其他读线程并发访问。但是它不支持锁升级先读锁后写锁因为这很容易导致死锁。公平性选择和ReentrantLock一样读写锁也可以选择公平或非公平模式权衡点类似。适用场景只有当读操作远大于写操作且读操作开销较大比如涉及网络、磁盘IO时使用读写锁带来的性能收益才明显。如果读操作本身非常快那么读写锁复杂的内部状态管理开销可能会抵消其带来的好处。5.2 乐观锁一种无锁的思维我们前面讨论的synchronized、ReentrantLock、ReadWriteLock都属于悲观锁。它们的思想是“只要我去操作共享数据就一定会有人来跟我抢所以我要先上锁把别人都挡在外面。”而乐观锁的思想恰恰相反“我认为在我操作期间别人大概率不会来修改这个数据所以我先放心去改改完之后再验证一下这期间数据有没有被别人动过。如果没动过就提交更新如果动了就放弃重试。”乐观锁通常不直接“锁”住资源而是通过一个版本号或时间戳来实现。在数据库中非常常见。数据库乐观锁示例假设有一张商品库存表product有id,stock,version字段。线程A查询商品SELECT id, stock, version FROM product WHERE id 1;得到stock10, version1。线程B也查询到同样的数据。线程A扣减库存更新时带上版本号校验UPDATE product SET stock stock - 1, version version 1 WHERE id 1 AND version 1;如果更新成功影响行数为1。线程B也尝试扣减执行同样的UPDATE语句但此时version已经是2了所以WHERE条件不成立更新影响行数为0。线程B就知道更新失败需要重新读取数据并重试业务逻辑比如提示用户库存不足或者重新计算。在Java中AtomicInteger、AtomicLong等原子类以及CAS操作就是乐观锁思想的体现。它们利用CPU底层的Compare-And-Swap指令在无锁的情况下实现线程安全的更新。AtomicInteger atomicCount new AtomicInteger(0); public void safeAdd() { int oldValue, newValue; do { oldValue atomicCount.get(); // 获取当前值 newValue oldValue 1; // 计算新值 } while (!atomicCount.compareAndSet(oldValue, newValue)); // CAS操作如果当前值还是oldValue就更新为newValue }乐观锁的优缺点优点在真正冲突很少的场景下性能极高因为它避免了线程挂起、上下文切换等重量级操作。缺点如果冲突真的经常发生写操作频繁那么CAS操作会频繁失败导致线程不断重试自旋反而会消耗大量CPU资源。这就是ABA问题的根源之一虽然AtomicStampedReference可以解决ABA问题但重试开销依然存在。如何选择悲观锁还是乐观锁这是一个经典的权衡。一个简单的判断依据是冲突的概率。如果冲突概率高写多读少悲观锁如ReentrantLock更合适因为它能让失败的线程直接排队等待而不是空转消耗CPU。如果冲突概率低读多写少乐观锁如CAS、版本号的性能优势会非常明显。在数据库层面SELECT ... FOR UPDATE是悲观锁而基于version的更新是乐观锁。你的业务场景决定了你的选择。6. 分布式锁跨越JVM的协同当你的应用从单机扩展到多机部署在多个JVM实例上时之前讨论的所有“本地锁”都失效了。因为它们只能控制单个JVM进程内的线程。此时你需要一个所有JVM实例都能访问的外部协调服务来实现锁这就是分布式锁。6.1 分布式锁的核心要求一个可靠的分布式锁至少需要满足以下几点互斥性在任意时刻只有一个客户端能持有锁。避免死锁锁必须有超时或自动释放机制防止客户端崩溃后锁永远无法释放。容错性提供锁服务的存储系统如Redis、ZooKeeper部分节点宕机时锁机制仍然能正常工作或快速恢复。高性能与高可用获取和释放锁的操作要快锁服务本身要可用。6.2 基于Redis的分布式锁实现与陷阱Redis因其高性能和丰富的数据结构成为实现分布式锁的热门选择。但实现一个生产可用的Redis分布式锁远不止一个SETNX命令那么简单。一个基础但问题重重的版本// 错误示范问题很多。 public boolean tryLock(String key) { return redisTemplate.opsForValue().setIfAbsent(key, locked); } public void unlock(String key) { redisTemplate.delete(key); }这个实现有致命缺陷锁无法释放如果获取锁的客户端在执行任务时崩溃这个锁就永远留在Redis里其他客户端再也无法获取死锁。非原子性setIfAbsent和expire如果不是原子操作可能在设置过期时间前客户端崩溃。误删他人锁客户端A超时释放了锁但此时它还在执行任务。客户端B获得了锁。客户端A任务执行完调用unlock把客户端B的锁给删了相对可靠的实现Redlock算法简化版通常我们使用SET key value NX PX milliseconds命令将设置值和过期时间作为一个原子操作。public boolean tryLock(String lockKey, String clientId, long expireTime) { // NX: 仅当key不存在时设置。 PX: 设置过期时间单位毫秒。 String result redisTemplate.execute((connection) - { JedisCommands commands (JedisCommands) connection.getNativeConnection(); return commands.set(lockKey, clientId, SetParams.setParams().nx().px(expireTime)); }); return OK.equals(result); } public boolean unlock(String lockKey, String clientId) { // 使用Lua脚本保证原子性只有锁的值是自己设置的才能删除 String luaScript if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; Long result redisTemplate.execute((connection) - { Object nativeConnection connection.getNativeConnection(); // 这里需要根据不同的Redis客户端Lettuce/Jedis进行适配 if (nativeConnection instanceof Jedis) { return (Long) ((Jedis) nativeConnection).eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(clientId)); } return 0L; }); return result ! null result 0; }这里clientId可以用UUID或“机器ID线程ID”来生成确保全局唯一。解锁时通过Lua脚本原子性地比较并删除防止误删。踩坑实录Redis分布式锁的“时钟漂移”与“主从切换”即使你实现了上面的逻辑在极端情况下依然可能出问题。比如Redlock算法就讨论了时钟跳跃、GC停顿等带来的风险。更常见的是Redis主从架构下的问题客户端在主节点上成功加锁但锁信息还未同步到从节点主节点就宕机了。从节点升级为主节点后这个锁就“丢失”了另一个客户端可能再次加锁成功导致互斥性被破坏。对于要求绝对正确性的场景如金融交易Redis分布式锁需要谨慎评估。此时像ZooKeeper、etcd这类强一致性的协调服务可能是更好的选择虽然它们的性能通常低于Redis。6.3 分布式锁与本地锁的协同在复杂的微服务架构中一种常见的优化模式是“两级锁”先用本地锁如ReentrantLock在JVM内部进行一轮粗粒度的同步竞争胜出的线程再去尝试获取分布式锁。这样可以极大地减少对分布式锁服务的请求压力因为大部分冲突在本地就被消化掉了。当然这要求你的业务数据在JVM层面有合理的分区或副本使得本地锁能覆盖大部分场景。7. 锁与事务的暧昧关系Transactional 与 synchronized这是一个非常容易混淆和出错的地方。很多初学者认为给一个方法加上Transactional注解和synchronized关键字就能万无一失地保证数据一致性。大错特错Service public class ProblematicService { Autowired private AccountRepository repository; Transactional public synchronized void transfer(int fromId, int toId, BigDecimal amount) { // 1. 查询账户A Account fromAccount repository.findById(fromId).orElseThrow(...); // 2. 查询账户B Account toAccount repository.findById(toId).orElseThrow(...); // 3. 计算并更新 fromAccount.setBalance(fromAccount.getBalance().subtract(amount)); toAccount.setBalance(toAccount.getBalance().add(amount)); // 4. 保存 repository.save(fromAccount); repository.save(toAccount); } }这段代码的问题在于锁的范围synchronized锁住的是this即当前Spring代理对象通常是一个单例Bean。它只能防止同一个JVM内多个线程同时执行这个transfer方法。在集群环境下其他JVM实例上的线程完全不受限制。事务边界Transactional的事务是在方法执行后才提交的。synchronized代码块执行完毕锁就释放了。但此时数据库事务可能还未提交。另一个线程拿到锁后读取到的可能还是旧数据取决于数据库的隔离级别如“可重复读”能避免但“读已提交”不能。执行顺序Spring的AOP代理会先处理Transactional再处理synchronized。这意味着线程进入synchronized块时事务已经开启了。如果锁内逻辑执行时间很长就会导致数据库连接被长时间占用增加死锁风险和连接池压力。正确的做法是什么明确职责synchronized用于解决单机JVM内的线程安全问题。Transactional和数据库锁如SELECT ... FOR UPDATE用于解决数据库层面的数据一致性问题。将锁粒度细化到数据本身对于转账这类业务应该去锁数据库里具体的账户行而不是锁整个Java方法。这就是悲观锁在数据库层的应用。Transactional public void transfer(int fromId, int toId, BigDecimal amount) { // 使用悲观锁锁定账户行 Account fromAccount repository.findWithLockingById(fromId); // 自定义查询使用 FOR UPDATE Account toAccount repository.findWithLockingById(toId); // ... 后续操作 }或者使用乐观锁通过版本号控制。分布式场景如果服务是集群部署必须使用分布式锁如基于Redis的锁来替代synchronized在访问数据库前先在所有JVM实例间竞争全局锁。记住一个原则Java代码里的锁管不了数据库数据库里的事务也管不了Java线程。你需要根据数据一致性的范围单机还是集群和强度要求来组合使用它们。8. 性能调优与避坑指南聊了这么多锁的原理和用法最后我们落到实际如何写出高性能、少坑的并发代码8.1 诊断锁竞争你的系统慢在哪当系统变慢怀疑是锁竞争时可以按以下步骤排查使用jstackjstack pid可以打印线程栈。重点关注BLOCKED状态的线程看它们阻塞在哪个锁上waiting to lock 0x000000071ad8fc50这样的信息。使用JVM参数-XX:PrintConcurrentLocks在jstack输出中显示更多的锁信息和-XX:PrintSafepointStatistics了解安全点停顿有些GC会触发STW需要所有线程到达安全点此时若线程持有锁不释放会加剧停顿。使用Profiler工具JProfiler、VisualVM、Async-Profiler可以图形化地显示线程状态、锁等待时间、热点方法等是定位性能问题的利器。监控关键指标监控系统的线程数、CPU使用率、GC时间。如果CPU使用率高但吞吐量低且有很多线程处于BLOCKED或WAITING状态锁竞争很可能是罪魁祸首。8.2 减少锁竞争的实战技巧缩小锁粒度这是最有效的办法。不要动不动就锁整个方法或整个对象。思考哪些变量是真正需要共享的只锁住访问这些变量的最小代码块。可以考虑使用锁分段技术例如ConcurrentHashMap在JDK 7中的实现将数据分成多个段每段一把锁。缩短锁持有时间在锁内只做必要的操作。任何耗时的操作如IO、网络调用、复杂计算尽量移到锁外执行。例如先从共享集合里把数据拷贝到局部变量然后尽快释放锁再对局部变量进行处理。尝试无锁编程对于计数器、累加器等场景优先考虑AtomicInteger、LongAdder在高并发下性能更好。对于复杂的状态更新可以研究CAS循环或Unsafe类需谨慎。使用并发容器ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue等容器在内部实现了高效的并发控制多数情况下比你自己用synchronized包装HashMap、ArrayList要好得多。避免嵌套锁尽量避免在持有一个锁的同时去获取另一个锁这是死锁的温床。如果无法避免务必保证所有线程以相同的顺序获取锁锁顺序一致性。考虑读写分离如果确实是读多写少的场景果断使用ReadWriteLock或StampedLockJDK 8引入提供乐观读模式性能更好。8.3 关于“无锁”的迷思“无锁编程”听起来很高大上但它不是银弹。CAS操作在高度竞争下会导致大量的CPU空转忙等待。LongAdder通过内部分段计数类似锁分段来缓解这个问题它在高并发写场景下比AtomicLong性能好但在低竞争或读多场景下开销反而更大。选择哪种并发控制方式永远是一个权衡。没有最好的只有最适合当前场景的。我的经验是在项目早期优先使用更简单、更不易出错的同步方式如synchronized或并发容器。当性能测试或监控明确指向锁是瓶颈时再考虑进行更复杂的优化如锁粒度拆分、使用ReentrantLock或ReadWriteLock甚至无锁数据结构。过早优化是万恶之源这句话在并发领域尤其正确。锁是并发编程中最强大也最危险的工具之一。理解其原理谨慎地使用并结合监控和测试才能构建出既正确又高性能的系统。希望这篇长文能帮你理清Java中“锁”的脉络下次当你再遇到synchronized、ReentrantLock或者“分布式锁”时能多一份从容和底气。