目录
Monitor(重量级锁)
对象头
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级锁)之后,在该对象头的 Mark Word 中设置指向该 Monitor 的指针
Mark Word(64 位)
锁不同时,Mark Word 会发生变化
Klass Word 指向该对象的类型
组成
WaitSet:线程得到锁,但不满足执行条件,于是进入 WaitSet 等到满足条件并被唤醒,进入 EntryList 等待
EntryList:当锁已被其他线程获取,进入 EntryList 等待锁被释放并被唤醒,开始不公平竞争锁(不是先来后到)
Owner:指向当前锁的主人
工作机制
① Thread1 执行到 synchronized(object),此时 Monitor 的 owner 为空,接下来 owner 指向 Thread1
② 接着,Thread0、Thread2 也执行到 synchronized(object),此时 owner 不为空,则它们进入 EntryList 等待
③ Thread1 临界区代码执行完毕,释放锁并唤醒 EntryList 中线程,进行不公平竞争;竞争成功的,owner 指向该线程;竞争失败的,再次进入 EntryList
④ Thread3、Thread4 之后都先后得到了锁,但是它们执行条件未满足(没有视频会员无法跳过广告),进入 WaitSet 等待
轻量级锁
① 对于同一把锁,多个线程间没有竞争;即它们用锁的时间是错开的,某段时间内只有一个线程使用这把锁;这时可以使用轻量级锁来优化
② 轻量级锁无需其他操作,同样是 synchronized;一开始加锁都是使用轻量级锁,假如加锁失败,说明存在竞争;此时转为重量级锁(Monitor)
加锁过程
① 线程创建锁记录对象(Lock Record),每个线程的栈帧都包含锁记录这个结构,锁记录内部可以保存对象的 Mark Word
② 锁记录中 Reference Object 指向对象
③ 锁记录的地址和状态 00 与对象的 Mark Word 尝试用 CAS 操作(原子性操作)进行交换;若交换成功,代表线程对对象加锁成功(线程获取锁成功)
④ CAS 操作失败,代表已有线程获取该对象的轻量级锁;此时表示有竞争,进入锁膨胀
或者发生了锁重入
或者
锁重入
上述 CAS 操作失败原因之一
public void method1(){ synchronized(object){ method2(); } } public void method2(){ synchronized(object){ // 执行代码 } }
① 当前线程多次获取同一把锁,这种操作就是锁重入;同样会产生锁记录,但是只用来计数
② 当解锁(释放锁)时,如果有取值为 null 的锁记录,表示有锁重入;这是重置锁记录,表示锁重入数减 1
③ 当遇到取值不为 null 的锁记录时,再次尝试使用 CAS 操作将原本 Mark Word 中内容替换回去;假如成功,表示解锁(释放锁)成功
④ CAS 操作失败时,表示锁进行了锁膨胀或者已经升级为重量级锁(Monitor)
锁膨胀(转为重量级锁)
上述 CAS 操作失败原因之二,轻量级锁解锁失败原因之一
① 由于竞争,锁升级为重量级锁(Monitor);此时 Monitor 的 owner 为 Thread0
② 按之前的说法,Thread1 应该进入 EntryList 等待;等到 Thread0 释放完锁,唤醒 EntryList 中线程,Thread1 才有机会获得锁???
自旋
Thread1 不会马上进入 EntryList 等待
它可以进行自旋重试;假如这时,Thread0 释放了锁,Thread1 可以获得到锁,避免阻塞
自旋重试成功:
Thread0(core0 上) Mark Word Thread1(core1 上) 尝试获取锁(monitor) 10 重量级锁 获取锁成功 10 重量级锁(重量级锁指针) 执行代码 10 重量级锁(重量级锁指针) 执行代码 10 重量级锁(重量级锁指针) 尝试获取锁(monitor) 执行代码 10 重量级锁(重量级锁指针) 自旋重试 执行代码 10 重量级锁(重量级锁指针) 自旋重试 执行代码 10 重量级锁(重量级锁指针) 自旋重试 执行完毕 10 重量级锁(重量级锁指针) 自旋重试 释放锁成功 001 无锁 自旋重试 10 重量级锁(重量级锁指针) 获取锁成功 10 重量级锁(重量级锁指针) 执行代码 10 重量级锁(重量级锁指针) 执行代码 ...... ...... 自旋重试失败(Thread0 执行时间过长,一直未释放锁):
Thread0(core0 上) Mark Word Thread1(core1 上) 尝试获取锁(monitor) 10 重量级锁 获取锁成功 10 重量级锁(重量级锁指针) 执行代码 10 重量级锁(重量级锁指针) 执行代码 10 重量级锁(重量级锁指针) 尝试获取锁(monitor) 执行代码 10 重量级锁(重量级锁指针) 自旋重试 执行代码 10 重量级锁(重量级锁指针) 自旋重试 执行代码 10 重量级锁(重量级锁指针) 自旋重试 执行代码 10 重量级锁(重量级锁指针) 自旋重试 执行代码 10 重量级锁(重量级锁指针) 自旋重试 执行代码 10 重量级锁(重量级锁指针) 自旋重试 执行代码 10 重量级锁(重量级锁指针) 自旋重试 执行代码 10 重量级锁(重量级锁指针) 阻塞 ...... ......
自旋注意事项
1、自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 才能发挥优势
① 因为,单核 CPU 只能并发执行,当 Thread1 获取时间片进行自旋时;Thread0 无法执行代码,更别说释放锁了;所以单核 CPU 自旋纯粹浪费
② 多核 CPU 可以让 Thread0、Thread1 并行执行;Thread0 执行代码同时,Thread1 在自旋
2、在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
3、Java 7 之后不能控制是否开启自旋功能
4、无竞争时,重量级锁会自动降为轻量级锁