Java所有的锁:从基础到进阶

作为Java开发者,"锁"是绕不开的核心知识点------不管是日常开发中的线程安全,还是面试时的高频追问(比如synchronized升级、CAS原理、锁的选型),掌握所有锁的特性和适用场景,才能真正吃透并发编程。

很多人对锁的认知,只停留在"synchronized和Lock"的表面,分不清公平锁与非公平锁、乐观锁与悲观锁,更不懂锁升级的逻辑,导致开发中选型失误、性能瓶颈,面试时被问得哑口无言。

今天这篇文章,一次性讲透Java中所有锁!按"认知→分类→原理→实战→避坑"的逻辑,从基础到进阶,覆盖所有高频锁类型,结合代码示例和面试考点,看完直接拿捏锁的核心知识,再也不用零散查资料。

为什么需要锁?

在Java并发编程中,线程安全的核心诉求是解决"多线程并发访问共享资源"的冲突问题------比如多个线程同时修改同一个变量、操作同一个文件,会导致数据不一致、竞态条件等问题。

锁的本质,就是通过"限制共享资源的并发访问",保证同一时刻只有符合条件的线程能操作资源,从而避免上述问题。简单说,锁就是并发编程的"线程安全守护神"。

随着Java并发体系的完善,锁的分类越来越丰富,不同类型的锁适用于不同的并发场景。我们按"高频度、高实用性"的维度,逐一拆解所有锁类型,拒绝模糊表述,全程干货无废话。

第一维度:按同步语义分类(核心!面试必问)

这是锁最核心的分类,核心区别在于"对并发冲突的预期",直接决定了锁的底层实现逻辑与性能特性,也是日常开发中锁选型的核心依据。

1. 悲观锁:假设冲突一定会发生

核心语义:先加锁,再操作。悲观锁认为,多线程并发时,数据修改冲突是必然的,所以在操作共享资源前,必须先获取锁,阻塞其他线程,直到当前线程操作完成并释放锁。

底层实现:依赖操作系统的互斥量(Mutex)或信号量,存在内核态与用户态的上下文切换开销,性能相对较低,但安全性极高。

典型实现:

  • synchronized:Java原生关键字,JVM层面实现,自动加锁、释放锁,无需手动管理,上手简单。

  • ReentrantLock:JUC包下的可重入锁,默认是悲观锁语义,支持手动加锁、释放锁,提供中断、超时等高级功能。

  • ReentrantReadWriteLock.WriteLock(写锁):读写锁中的写锁,属于悲观锁的一种,排他性强。

代码示例(synchronized与ReentrantLock):

适用场景:高冲突、写操作频繁的场景(如金融转账、库存扣减),安全性优先于性能。

2. 乐观锁:假设冲突不会发生

核心语义:先操作,后校验。乐观锁认为,多线程并发时,数据修改冲突的概率很低,所以无需提前加锁,直接操作共享资源,仅在提交操作时检查资源是否被修改。

底层实现:基于CAS(Compare and Swap,比较并交换)原子操作或版本号机制,无阻塞、无上下文切换开销,性能优于悲观锁,但需要处理冲突重试逻辑。

典型实现:

  • Atomic系列原子类(AtomicInteger、AtomicLong等):底层基于CAS实现,无需加锁,保证并发安全。

  • ConcurrentHashMap:分段锁+CAS结合,兼顾并发性能和安全性。

  • StampedLock:JUC包下的高级锁,支持乐观读模式,性能优于传统读写锁。

  • 版本号/时间戳机制:常见于数据库乐观锁(如JPA的@Version注解)。

代码示例(AtomicInteger与版本号机制):

面试高频提醒:乐观锁并非"无锁",而是"无阻塞锁";悲观锁并非"低效锁",而是"兜底锁",二者无绝对优劣,核心是根据并发冲突频率选型。

适用场景:低冲突、读操作频繁的场景(如缓存更新、计数器),性能优先于安全性。

第二维度:按锁的排他性分类

核心区别在于"同一时刻,允许多少个线程获取锁",直接决定了锁的并发能力,也是JUC包下锁的核心设计维度(基于AQS的两种核心模式)。

1. 独占锁(排他锁):同一时刻仅一个线程持有

核心语义:同一时刻,只有一个线程能获取锁,其他线程获取失败则进入等待队列,具有强排他性(类似"一人使用,其他人排队"),主要用于写操作。

底层依赖:基于AQS的独占模式(tryAcquire/tryRelease钩子方法)。

典型实现:synchronized、ReentrantLock、ReentrantReadWriteLock.WriteLock(写锁)。

核心特点:安全性高,但并发能力弱,适合写操作场景(如修改数据、删除数据)。

2. 共享锁:同一时刻多个线程持有

核心语义:同一时刻,允许多个线程同时获取锁,无排他性(类似"多人同时阅读同一本书"),仅阻止写线程获取锁,主要用于读操作。

底层依赖:基于AQS的共享模式(tryAcquireShared/tryReleaseShared钩子方法)。

典型实现:ReentrantReadWriteLock.ReadLock(读锁)、Semaphore(信号量)、CountDownLatch(倒计时器)。

核心特点:并发能力强,适合读多写少场景(如查询数据、统计数据)。

第三维度:按锁的公平性分类

核心区别在于"线程获取锁的顺序",即是否遵循"先到先得(FIFO)"原则,影响锁的公平性与性能,面试中经常被问"synchronized是公平锁还是非公平锁"。

1. 公平锁:先到先得,拒绝插队

核心语义:线程获取锁的顺序,严格遵循"FIFO"原则,先请求锁的线程先获取锁,不允许"插队",能避免线程饥饿(长期得不到锁)。

底层实现:线程获取锁时,先检查等待队列是否有前驱线程,若有则排队,无则尝试获取锁。

典型实现:ReentrantLock(构造方法传入true)、ReentrantReadWriteLock(公平模式)。

核心特点:公平性高,但频繁切换线程会增加性能开销,吞吐量较低,适合对公平性要求高的场景(如金融交易)。

2. 非公平锁:允许插队,优先性能

核心语义:线程获取锁时,不遵循FIFO原则,允许"插队"------刚释放锁的线程可再次立即获取锁,无需排队,减少线程切换开销。

底层实现:线程获取锁时,先尝试CAS获取锁,失败再加入等待队列,减少线程切换开销。

典型实现:synchronized(默认非公平)、ReentrantLock(默认非公平,构造方法传入false)。

核心特点:公平性低,可能导致线程饥饿,但性能高、吞吐量高,适用于大多数并发场景(日常开发默认选择)。

第四维度:按锁的重入性分类

核心区别在于"持有锁的线程是否能再次获取该锁",避免线程自身死锁,是锁的基础特性,很多新手容易忽略这一点。

1. 可重入锁:持有锁的线程可再次获取

核心语义:持有锁的线程,可再次获取该锁(重入),锁内部会记录"重入次数",释放时需释放对应次数,直到重入次数为0,锁才真正释放。

底层实现:通过记录"持有锁的线程ID"和"重入次数",避免线程自身死锁。

典型实现:synchronized(可重入)、ReentrantLock(可重入,名字就带Reentrant)、ReentrantReadWriteLock。

核心优势:避免线程自身死锁,简化嵌套同步逻辑(如方法A调用方法B,二者均加锁,可重入锁不会导致死锁)。

代码示例(可重入锁嵌套):

2. 不可重入锁:持有锁的线程无法再次获取

核心语义:持有锁的线程,无法再次获取该锁,若尝试获取,会导致自身死锁。

典型实现:JUC包下无直接实现,自定义锁可实现不可重入语义(如简单的CAS锁)、Linux下的pthread_mutex_t(默认不可重入)。

核心劣势:易导致线程自身死锁,适用场景极少,仅用于特殊同步需求(日常开发几乎不用)。

第五维度:按底层实现分类(JVM锁升级重点)

核心区别在于"锁的实现层面",直接决定锁的性能开销,尤其是synchronized的锁升级机制,是面试高频难点(JDK1.6后重大优化)。

核心前提:锁升级是不可逆的!一旦从偏向锁升级到轻量级锁,就不能再回退;一旦升级到重量级锁,就只能一直保持重量级锁状态。

1. 偏向锁:无竞争场景的最优解

核心场景:当只有一个线程反复获取、释放同一把锁,没有任何其他线程参与竞争时(如单线程执行同步代码块),JVM会自动使用偏向锁。

核心原理:"偏心"绑定线程------JVM在对象头(Mark Word)中记录当前持有锁的线程ID,后续该线程再获取锁时,无需做任何同步操作(不用CAS、不用切换内核态),直接进入同步代码块,开销几乎可以忽略不计。

优势与不足:开销极小,适合无竞争场景;但一旦有其他线程参与竞争,需撤销偏向锁,触发升级,撤销过程有一定开销。

2. 轻量级锁:轻度竞争场景的过渡方案

核心场景:有少量线程交替获取锁(没有同时争抢,即"自旋竞争"),此时偏向锁被撤销,升级为轻量级锁。

核心原理:线程进入同步代码块前,会在自己的栈帧中创建"锁记录"(Lock Record),记录锁对象的Mark Word副本;通过CAS操作,将锁对象的Mark Word更新为自己的锁记录地址,CAS成功则获取锁,失败则进入自旋(不断尝试CAS)。

优势与不足:无需切换内核态,开销远小于重量级锁;但自旋会消耗CPU资源,若锁持有时间过长,自旋会变成无效消耗,触发升级为重量级锁。

3. 重量级锁:重度竞争场景的终极保障

核心场景:有大量线程同时争抢同一把锁(重度竞争),或锁被持有时间很长,自旋无法高效获取锁时,轻量级锁升级为重量级锁。

核心原理:依赖操作系统的互斥量(Mutex Lock),属于内核态锁。线程获取锁失败时,不再自旋,而是直接放弃CPU执行权,进入操作系统的阻塞队列,直到持有锁的线程释放锁,操作系统才会唤醒队列中的线程。

优势与不足:稳定性高,能应对重度竞争;但开销最大,每次获取、释放锁都需要切换用户态和内核态,线程阻塞/唤醒开销大。

锁升级完整流程(必背)

无锁 → 偏向锁(无竞争) → 轻量级锁(轻度竞争,CAS+自旋) → 重量级锁(重度竞争,阻塞等待),一旦升级,无法回退。

第六维度:其他高频锁类型(面试拓展)

1. 自旋锁:轻量级锁的核心实现

核心语义:线程获取锁失败时,不立即阻塞,而是循环尝试获取锁(自旋),直到获取成功或达到自旋阈值。

底层实现:基于CAS操作,无上下文切换开销,适合锁持有时间短、竞争不激烈的场景;自旋次数达到阈值(JVM默认10次),则升级为重量级锁。

注意:JDK1.6后引入"自适应自旋锁",自旋次数会根据之前的竞争情况动态调整,无需手动配置。

2. 分段锁:ConcurrentHashMap的核心优化

核心语义:将锁的粒度细化,把一个大的容器(如HashMap)分成多个段(Segment),每个段对应一把锁,多线程访问不同段时,无需竞争同一把锁,提高并发性能。

典型实现:JDK1.7的ConcurrentHashMap(JDK1.8后改为CAS+ synchronized,取消分段锁,但分段锁的思想仍需掌握)。

3. 无锁:乐观锁的极致形态

核心语义:完全不依赖锁机制,直接通过CPU原子指令(如CAS)或线程本地存储(ThreadLocal)保证线程安全,不存在锁竞争,性能最优。

典型实现:Atomic系列原子类、Unsafe.CAS、ThreadLocal(线程隔离,无共享则无竞争)。

注意:无锁并非"不需要保证安全",而是通过非锁机制实现安全,适用于线程间无共享数据修改或修改冲突极少的场景。

实战总结:锁的选型指南(开发/面试都能用)

掌握了所有锁的特性后,核心是"按需选型",避免盲目使用重量级锁,也避免滥用乐观锁导致冲突问题。整理了一张选型表,直接套用:

面试高频避坑点(必记)

  • 避坑1:synchronized不是只有重量级锁,JDK1.6后有偏向锁、轻量级锁、重量级锁三种状态,会自动升级。

  • 避坑2:乐观锁不是无锁,而是无阻塞锁,需要处理冲突重试逻辑,否则会导致数据不一致。

  • 避坑3:ReentrantLock必须在finally中释放锁,否则会导致锁泄漏(异常时无法释放锁)。

  • 避坑4:锁升级是不可逆的,一旦升级为重量级锁,就不会再回退到偏向锁或轻量级锁。

  • 避坑5:synchronized是自动重入、非公平锁,无法手动设置为公平锁;ReentrantLock可手动设置公平/非公平。

相关推荐
硅基诗人2 小时前
Java后端高并发核心瓶颈突破(JVM+并发+分布式底层实战)
java·jvm·分布式
聆听。。花开雨落2 小时前
intelij idea闪退后再启动tomcat报错端口冲突
java·tomcat·intellij-idea
Java面试题总结2 小时前
Spring Boot 包扫描新姿势:AutoScan vs @Import vs @ComponentScan 深度对比
java·数据库·spring boot
掘金者阿豪2 小时前
数据库安全第一关:用户密码存储与认证机制的深度拆解
java·前端·后端
花千树-0102 小时前
McpAgentExecutor 混合挂载:HTTP 工具与 NPX 服务器同时接入同一 Agent
java·agent·function call·spring ai·mcp·toolcall·java ai
XiYang-DING2 小时前
【Java】反射
java·开发语言
ACGkaka_2 小时前
JDK 版本管理工具介绍:jenv与sdkman(Mac端)
java·macos·sdkman
阿坤带你走近大数据2 小时前
数据API接口的数据源和目标源分别是什么?怎么设置?
java·python·api
若阳安好2 小时前
【java】任务流批处理平台
java·开发语言