一,悲观锁 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**. 无锁(初始):** 没有线程占有锁。
- 偏向锁(单线程反复获取,无竞争) :首个线程进入同步块,无竞争 → 对象头记录线程ID,变成偏向锁;后续同一线程再来直接拿锁,无CAS开销。
出现其他线程竞争:偏向撤销 → 升级轻量级锁。
- 轻量级锁(自旋锁,少量竞争): 线程用自旋CAS抢锁,不停循环尝试获取,不挂起线程。
JVM自适应自旋:根据前次自旋成功率,动态调整自旋次数:之前自旋成功:下次多自旋几次; 多次自旋拿不到锁:不再自旋。
4.重量级锁(高并发、自旋失败): 自旋耗CPU仍抢不到锁 → 升级OS重量级锁,抢不到的线程阻塞挂起,进入内核等待队列。
九,小结
这篇属于八股文,不用死记硬背但是也需要能理解掌握。看了别人的优秀博客,感觉自己写的还是不够详细,那就从这篇开始更注意一点吧。今天我六级阅读全对,耶耶耶!!