MyRecord
MyRecord copied to clipboard
并发中关于各种锁的解释
看到有人列出的大纲:
- [x] 自旋锁
- [ ] 自旋锁的其他种类
- [ ] 阻塞锁
- [x] 可重入锁
- [x] 互斥锁
- [x] 悲观锁
- [x] 乐观锁
- [x] 公平锁
- [x] 非公平锁
- [x] 偏向锁
- [ ] 对象锁
- [ ] 线程锁
- [x] 锁粗化
- [x] 轻量级锁
- [x] 重量级锁
- [x] 锁消除
- [x] 锁膨胀
- [ ] 信号量
- [x] 读写锁
- [x] 排它锁(X 锁)
- [x] 共享锁(S 锁)
- [x] 意向锁
大部分不了解,有部分名字很熟悉,完善各种锁的基本解释
通俗理解偏向锁
在 synchronized 块,当线程进入临界区,JVM 会申请锁, 也得和操作系统协商什么互斥量,从用户态进入核心态,再从核心态返回用户态....
Account account = ...
synchronized(account){
...临界区的代码...
}
JVM 只会让一个线程进入临界区执行。
引用自:https://mp.weixin.qq.com/s/sIMowR_qjskg-WeriMiIlg 在偏向锁中,JVM 老大根本没有去找操作系统, 只是看了看这个 account 对象的所谓“对象头”,其中有个叫做Mark Word 的东西,是个什么数据结构, 里边有几个标识位,还有其他数据。
下面的图片有一个错误,偏向锁对象头里存的不是线程 id ,其实存的是 JavaThread 对象的指针地址。
老大随手使用 CAS 操作把我的线程 ID 记录到了这个 Mark Word 当中,修改了标识位,然后告诉我说: 可以了,你现在拥有这把锁了,进去执行代码吧。 我惊奇地说:“老大你不和操作系统协商设置 Mutex 了?”
老大说:“不用了,你看现在就你一个线程进入了这个代码块,我只要记录下你的线程 ID,就表示你拥有这把锁了,不用操作系统介入。”
我获得了锁,开始执行被 synchronized 包裹的代码块。
等到我第二次执行到这个 synchronized 的时候,老大只是看了一眼锁对象 account 的 Mark Word 就说:“你的线程 ID 还在,还持有着这个对象的锁,进入临界区执行吧。”
老大说,这叫偏向锁,在没有别的线程竞争的时候,一直偏向我,可以让我一直执行下去。
通俗理解轻量级锁
接上文
另外一个线程 0x3704 也要进入这个代码块执行,但是锁对象 account 保存的是我的线程ID,他是没法进入临界区的。
我心想,我们两个至少得有一个进入阻塞状态,休息一会儿了。
但是老大还是不去操作系统协商,只是说: 我把这个偏向锁升级一下,变成一个轻量级的锁吧。
老大把锁对象 account 恢复成无锁状态,在我们俩的栈帧中各自分配了一个空间,叫做 Lock Record, 把锁对象 account 的 Mark Word 在我们俩这里各自复制了一份,叫做 Displaced Mark Word,这名字真奇怪。
然后把我的 Lock Record 的地址使用 CAS 放到了 Mark Word 当中,并且把锁标志位改为 00, 这其实就意味着我也已经获得了这个轻量级的锁了,可以继续进入临界区执行。
0x3704 没有获得锁,但还是不阻塞,老大让他自旋几次,等待一会儿。
等到我退出临界区,释放锁的时候,需要把这个 Displaced markd word 使用 CAS 复制回去。接下来他就可以加锁了。
我们两个交替着进入临界区,执行这段代码,相安无事,很少出现真正的竞争。
即使是出现了竞争,想获得锁的线程只要自旋几次,等待一会儿,锁就可能释放了。
很明显,如果没有竞争或者轻度的竞争,轻量级锁仅仅使用 CAS 操作和 Lock record 就避免了重量级互斥锁的开销,对 JVM 老大来说,确实是个好主意。
通俗理解重量级锁
接上文
轻量级锁运行得挺好,我还是没有机会休息,终于有这么一天,0x3704 正在持有锁,在临界区辛苦地执行代码。 我自旋了好多次,0x3704 还是没释放锁。 这时候 JVM 老大说: 自旋次数太多了,太浪费 CPU 了,接下来升级为重量级锁!
这个重量级锁需要操作系统的帮忙,依赖操作系统底层的 Mutex Lock。
只见老大创建了一个 monitor 对象, 把这个对象的地址更新到了 Mark word 当中。
锁升级了!
由于0x3704还在持有锁运行,而我终于进入了梦寐以求的状态:阻塞! 终于可以休息一下了!
仔细一想,老大煞费心机地设置了偏向锁和轻量级锁,就是为了避免阻塞,避免操作系统的介入, 这两种锁无非就是针对这两种情况:
- 偏向锁: 通常只有一个线程在临界区执行
- 轻量级锁: 可以有多个线程交替进入临界区,在竞争不激烈的时候,稍微自旋等待一下就能获得锁。
- 至于重量级锁,也是我最为期待的锁,那就是出现了激烈的竞争,只好让我们去阻塞休息了。
关于上面的补充:
- 锁只能升级,不能降级
- 这几种锁的使用是 JVM 自动切换的
岂能让你轻易阻塞? 2333
读写锁
多是在数据库里使用,分为读锁和写锁:
- 排它锁(Exclusive),简写为 X 锁,又称写锁。
- 共享锁(Shared),简写为 S 锁,又称读锁。
有以下两个规定:
- 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
- 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。
锁的兼容关系如下:
- | X | S |
---|---|---|
X | × | × |
S | × | √ |
意向锁
使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。
在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。
意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:
- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。
各种锁的兼容关系如下:
- | X | IX | S | IS |
---|---|---|---|---|
X | × | × | × | × |
IX | × | √ | × | √ |
S | × | × | √ | √ |
IS | × | √ | √ | √ |
解释如下:
- 任意 IS/IX 锁之间都是兼容的,因为它们只是表示想要对表加锁,而不是真正加锁;
- S 锁只与 S 锁和 IS 锁兼容,也就是说事务 T 想要对数据行加 S 锁,其它事务可以已经获得对表或者表中的行的 S 锁。
所以,在意向锁存在的情况下,事务必须先申请表的意向共享锁,成功后再申请一行的行锁。 当发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,其他事务申请表的写锁(X 锁)会被阻塞。
注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。
我的理解:X 锁可以当作加在行上,表示为锁行;意向锁是一把虚拟锁,表示有人在操作某行,为了省去确定是那一行,直接用 IX 表示锁表;如果要加 X 锁,首先要获得 IX。 ( : 雾
作用:意向锁提高了锁定父节点时的效率(相当于一个全局标记位) 原理:可以直接通过目标节点的意向锁便得知是否可以对目标表进行加锁,而不需要遍历该节点的所有子节点。 场景:在支持多粒度的层级结构锁定的数据库中,锁定父节点时有效。
其他没有提到但是标记已完成的,参考 『深入JVM』和『并发编程的艺术』相关笔记