全面拆解 Java 锁:分类辨析 + 底层原理精讲

一,悲观锁 vs 乐观锁

描述的是加锁时遇到的场景,不是针对某种具体的锁,而是有这个特性

悲观锁:

加锁时总是预测接下来锁竞争非常激烈,即每次拿数据时都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁

**优点:**数据一致性极强,不会出现并发脏写

**缺点:**并发性能差,大量线程阻塞,高并发场景容易产生锁等待、死锁
乐观锁:

加锁时预测接下来锁竞争很不激烈,所以在数据进行提交更新时,才会对数据是否产生并发冲突进行检测,如果发现冲突,则返回用户错误信息,让用户决定怎么去做

优点: 无锁阻塞,并发吞吐量高,适合高并发场景

缺点: 冲突频繁时会大量更新失败,需要业务重试

举个栗子:去洗浴中心柜子存东西时

1. 悲观锁(凡事往坏处想,提前上锁占坑)

想法:我离开柜子这段时间,铁定有人偷偷开我柜子乱动东西

1,拿到柜子钥匙(上锁),钥匙攥手里不撒手;

2,别人哪怕只想打开柜门看一眼,没有钥匙就被拦在外面干等着;

3,等我用完东西、归还钥匙(解锁),下一个人才能用柜子。

对应:查数据立刻加锁,其他人全阻塞排队。

2. 乐观锁(默认没人乱动,最后结账才核对)

想法:大概率没人私自开我柜子,基本不会撞车

1,柜子随便开,所有人都能随便打开看东西,全程不用拿钥匙上锁;

2,只有我临走要改柜子里物品(更新数据)的时候,检查一遍:柜子里东西和我刚来的时候一样吗?

没变→顺利拿走 / 修改物品(更新成功);

被动过了→东西被人动了,操作作废,直接通知本人:被占用了,操作失败,你自己重新再来。

对应:查询不加锁,更新时校验版本,冲突直接报错。

Synchronized初始使用乐观锁策略,当发现锁竞争比较频繁时,会自动切换成悲观锁策略。可以说synchronized既是悲观锁又是乐观锁,jvm内部会安排好

二,重量级锁 vs 轻量级锁

可以认为是针对不同场景所设计的锁优化方案

锁的核心特性"原子性"是cpu这样的硬件设备提供的

  • cpu提供了"原子操作指令"
  • 操作系统基于cpu的原子指令,实现了mutex互斥锁
  • jvm基于操作系统提供的互斥锁,实现了synchronized和ReentrantLock等关键字和类

**重量级锁:**加锁机制重度依赖OS提供了mutex

大量的内核态用户态切换

很容易引发线程的调度

悲观锁在synchronized里落地就是重量级锁。同时付出更多代价,更低效
**轻量级锁:**加锁机制尽可能不使用mutex,而是尽量在用户态代码完成,实在搞不定再用mutex

少量的内核态用户态切换

不容易引发线程的调度

乐观锁在synchronized里落地就是轻量级锁。同时付出更少代价,更高效

三,挂起等待锁 vs 自旋锁

挂起等待锁: 重量级锁的典型实现。操作系统内核级别的,加锁时发现竞争,就会使该线程进入阻塞状态,后续需要内核进行唤醒

获取锁周期长,很难及时获取,但不必一直消耗cpu,可把cpu省出来做别的
自旋锁: 轻量级锁的典型实现。应用程序级别的,加锁时发现竞争,一般也不是进入阻塞,而是通过忙等的形式来进行等待

没有放弃cpu,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁。但如果锁被其他线程持有时间比较久,就会持续消耗cpu资源

举个栗子:想象一下, 去追求一个小帅. 当我向小帅表白后, 小帅说: 不好意思我们不合适

挂起等待锁: 陷入沉沦不能自拔... 过了很久很久之后, 突然小帅发来消息, "咱俩要不试试?" (注意, 这个很长时间的间隔里,小帅可能已经换了好几个女票了).

自旋锁: 死皮赖脸坚韧不拔. 仍然每天持续骚扰小帅. 一旦小帅表现出有谈恋爱的想法, 那么就能⽴刻抓住机会上位.

Synchronized中的轻量级锁策略大概率就是通过自旋锁方式实现的

四,普通互斥锁 vs 读写锁

普通互斥锁:

同一时间只允许一个线程拿到锁执行代码,其他抢锁线程全部阻塞等待,用来保护临界资源,防止多线程并发乱改数据。
读写锁:

读写锁就是在执行枷锁操作时需要额外表明读写意图,复数读者之间不互斥,而写者则要求与任何人互斥
一个线程对于数据的访问, 主要存在两种操作: 读数据写数据.

  • 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
  • 两个线程都要写一个数据, 有线程安全问题.
  • 一个线程读另外一个线程写, 也有线程安全问题

读写锁就是把读操作和写操作区分对待. Java标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法
进行加锁解锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock
方法进行加锁解锁.
其中(读加锁和读加锁之间, 不互斥. 写加锁和写加锁之间, 互斥. 读加锁和写加锁之间, 互斥)
synchronized不是读写锁

五,可重入锁 vs 不可重入锁

核心要点:

1.锁要记录当前是哪个线程拿了这把锁

2.使用计数器,记录当前加锁多少次,从而再合适的时间释放这把锁

可重入锁:

允许同一个线程多次获取同一把锁

比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)
不可重入锁:
一个线程第一次加锁成功,在进行第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不干了, 也就无法进行解锁操作. 这时候就会死锁。这样的锁就称为不可重入锁
synchronized是可重入锁

六,公平锁 vs 非公平锁

没有好坏之分,关键看适用场景

公平锁:

遵循**"先来后到** ",想实现公平锁需要付出额外的东西(如:使用队列记录线程获取锁顺序)
非公平锁:

不遵循**"先来后到**"

举个栗子:有A B C三个线程,A先尝试获取锁,获取成功。B再尝试获取锁, 获取失败, 阻塞等待; C也尝试获取锁, 获取失败, 阻塞等待。当A释放锁时,会发生什么呢

公平锁:B比C先来,所以A释放锁后,B先于C获取锁

非公平锁:B和C都有可能获取到锁

Synchronized是非公平锁

七,锁粗化

所谓的锁粗化,取决于锁的粒度 (加锁解锁间,包含代码越多,认为锁粒度越粗)。

一个代码中,反复针对细粒度的代码加锁,就可能被优化成更粗粒度的加锁。本来执行多次加锁解锁,优化成一次加锁解锁,每次加锁解锁后,重新加锁,会增加竞争

八,再谈synchronized

synchronized 锁自适应升级(单向不可逆

1**. 无锁(初始):** 没有线程占有锁。

  1. 偏向锁(单线程反复获取,无竞争) :首个线程进入同步块,无竞争 → 对象头记录线程ID,变成偏向锁;后续同一线程再来直接拿锁,无CAS开销。

出现其他线程竞争:偏向撤销 → 升级轻量级锁。

  1. 轻量级锁(自旋锁,少量竞争): 线程用自旋CAS抢锁,不停循环尝试获取,不挂起线程。

JVM自适应自旋:根据前次自旋成功率,动态调整自旋次数:之前自旋成功:下次多自旋几次; 多次自旋拿不到锁:不再自旋。

4.重量级锁(高并发、自旋失败): 自旋耗CPU仍抢不到锁 → 升级OS重量级锁,抢不到的线程阻塞挂起,进入内核等待队列。

九,小结

这篇属于八股文,不用死记硬背但是也需要能理解掌握。看了别人的优秀博客,感觉自己写的还是不够详细,那就从这篇开始更注意一点吧。今天我六级阅读全对,耶耶耶!!

相关推荐
曹牧1 小时前
Java:import NeverUsed
java·开发语言·log4j
之歆1 小时前
在 IntelliJ IDEA 里复刻 Cursor 式内联审查:架构复盘-从放弃到拾起:如何用 LineStatusTracker 拯救一个烂掉的项目
java·架构·intellij-idea
jeffer_liu1 小时前
Spring AI 生产级实战-结构化输出
java·人工智能·后端·spring·大模型
疏狂难除1 小时前
JetBrains IDE插件开发教程(四)——Action
java·ide·kotlin
laufing1 小时前
java web 基础 ---- servlet
java·servlet·web开发
程序猿乐锅1 小时前
【苍穹外卖|Day01】项目初识:从多模块结构到 OpenAPI 接口文档踩坑
java·spring·maven·mybatis
我不是懒洋洋1 小时前
【C++】内存管理与模板(C++内存管理方式、new和delete的实现原理、malloc/free和new/delete的区别、函数模板、类模板)
c语言·开发语言·c++·青少年编程·visual studio
雪的季节1 小时前
Qt多窗口架构设计需求简介
开发语言·qt
李白的天不白1 小时前
针对你遇到的 Client.Timeout exceeded 问题,我判断是防火墙拦截了 HTTPS 流量
java