Synchronized
使用
(1) synchronized 方法:
public class Bank { |
(2) synchronized 块:
Object object = new Object(); |
- 每个类对象都有从 Object 继承的”对象锁”, synchronized 方法利用这个对象锁保护方法内的代码片段.
- 对于同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前对象的 Class 对象。
- 对于同步方法块,锁是 Synchonized 括号里配置的对象。
实现
- 在 synchronized 代码块前后增加的
monitorenter
和monitorexist
两个 JVM 字节码指令,指令的参数是 this 引用。 - synchronized 关键字起到的作用是设置一个独占访问临界区,在进入这个临界区前要先获取对应的监视器锁,任何 Java 对象都可以成为监视器锁,声明在静态方法上时监视器锁是当前类的 Class 对象,实例方法上是当前实例。
- synchronized 提供了原子性、可见性和防止重排序的保证。
- JMM 中定义监视器锁的释放操作 happen-before 于后续的同一个监视器锁获取操作。再结合程序顺序规则就可以形成内存传递可见性保证。
下面以一段 Java 代码为例:
public class TestSynchronize { |
javap 查看 inc()
方法的实现:
private void inc(); |
在 synchronized 代码块前后增加的 monitorenter
和 monitorexist
两个 JVM 字节码指令,指令的参数是 this 引用。
hotspot 中对于 monitor_enter
字节码指令的实现(注,不同 JDK 版本代码可能不同):
|
从上面的 monitorenter
代码可以看到先尝试偏向锁(fast_enter),若失败则改为使用轻量级锁(slow_enter),在轻量级锁的代码中如果也失败,最后兜底方案是膨胀为重量级锁:
// slow_enter()函数是轻量级锁的实现,函数最后的inflate()是重量级锁 |
对象锁的升级
上面的代码看到了 monitor_enter
的大致流程:偏向锁(fast_enter)-》轻量级锁(slow_enter)-》重量级锁,前两者的实现都使用了 Java 对象的 Mark Word 来实现。
Java SE1.6 为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,
所以在 Java SE1.6里 对象锁
一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态 和 重量级锁状态,
它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
随着锁的升级, Java 对象头里的 Mark Word
存储的内容也会变化。
回顾 Java 对象的内存结构:
在 64 位 JVM 环境, Java 对象对象头中,前 64bit 是”Mark Word”, 存储了 hashCode, 锁信息, 以及分代信息等,
MarkWord 低 2 位是锁标记, 加上 1 位的偏向锁标记,一共对应 4 种锁:
图来自 https://juejin.cn/post/6844904114061590535#heading-5
无锁
无锁状态下, 对象 Mark Word 锁标志也是 01(同偏向锁一样);
偏向锁
在无锁状态下,当有线程尝试获取对象锁(前提是 JVM 开启了支持偏向锁),那么会通过 CAS 操作,将对象头的 Mark Word 替换为自己的线程 ID,并把 Mark Word 锁标志置为101,这时候对象锁进入偏向锁;
偏向锁状态下,线程尝试进入同步块的过程如下:
- 检查 Mark Word 中的 ThreadID 是否等于自己的,如果是则直接进入同步块;
- 如果不等于自己的 ThreadID,获取偏向锁失败
所以,对于很少出现竞争的同步块,偏向锁的效率很高(只是简单检查)
偏向锁不会立刻撤销,而是等到下次全局安全点(safe point,当前 JVM 没有正在执行的字节码),JVM 会检查持有偏向锁的线程是否还活着,
- 如果否,会设置 Mark Word 为无锁状态,随后偏向新的线程;
- 如果是,已持有偏向锁的线程继续执行,竞争失败的线程开始自旋+CAS(轻量级锁)
可以看到偏向锁是一直等到有竞争才会释放,偏向锁的撤销需要等到下个 safe point。
是否要用偏向锁,取决于:
- 偏向锁在 JDK 6及以后的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:
-XX:-UseBiasedLocking=false
; - 如果对象已经调用过 hashCode 方法,Mark Word 中会存储 hashcode 的值,这种对象就无法转换为偏向锁了,而是直接从无锁转换为轻量级锁,但这对轻量锁和重量所没关系,因为这两种锁会各自保存 HashCode(前者会保存在栈帧中的 Lock Record,后者保存在 Monitor 对象中)@ref: Advanced-Java.05.对象内存结构
偏向锁中的 epoch 解析: 偏向锁里面的epoch作用_Java多线程-IT乾坤技术博客
轻量级锁
上面提到,如果线程 A 已经持有了偏向锁,线程 B 尝试解锁的时候会使用 CAS,也即进入了轻量级锁,轻量级锁的实现是自旋+CAS,
- 竞争线程会在自己的栈帧中创建 Lock Record(锁记录),并将对象头中的 Mark Word 复制到 Lock Record
- 竞争线程使用 CAS,尝试将对象头的 Mark Word 替换为指向 Lock Record 的指针
- CAS 失败则自旋获取锁,自旋超过一定次数,对象锁升级为重量级锁
上面的自旋次数默认是10次,可以使用-XX:PreBlockSpin 来更改,自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
@doubt: 当是偏向锁时,另一个线程 B 尝试 CAS,CAS 的 exceptVal 是“无锁状态下的 MarkWord”?那么在 B 中如何获取“无锁状态下的 MarkWord”?
重量级锁
对象锁升级为重量级锁:Mark Word 的锁标记设置为 00,Mark Word 中指向 Lock Record 的指针被替换为指向重量级锁的指针,
图片来自 Synchronization - Synchronization - OpenJDK Wiki
重量级锁解析
上面提到了轻量级锁在自旋失败后,会膨胀为重量级锁,重量级锁是由 ObjectMonitor 对象实现的,每个 Java 对象升级到重量级锁后,Mark Word 内会存储指向 ObjectMonitor 对象的指针。
先看 ObjectMonitor 的主要成员:
ObjectMonitor() { |
_recursions
:如果线程 a 已经持有 monitor 锁的情况下,再次加锁,recursions+1(类似 ReentrantLock 的 State 计数器)_owner
:初始时为 NULL。当有线程占有该 monitor 时 owner 标记为该线程的 ID。当线程释放 monitor 时 owner 恢复为 NULL。_owner
是一个临界资源 JVM 是通过 CAS 操作来保证其线程安全的(类似 ReentrantLock 的 exclusiveOwnerThread)_EntryList
: 竞争队列,因抢锁失败的线程会进入这个队列(类似 ReentratLock 的“同步队列”)_WaitSet
: 等待队列,因调用 wait 方法而被阻塞的线程会被放在该队列中( 类似 Condition 对象的“等待队列”)
【图】 Monitor 模型:
- 抢锁的线程先进入 EntrySet
- EntrySet 队列中的线程尝试
acquire()
锁,成功后改写 Owner 为线程自己, - 持有锁的线程调用了
wait()
,会释放锁并进入 WaitSet - WaitSet 中的线程,被其他线程
notify()
唤醒后,会尝试acquire()
锁 - 持有锁的线程释放锁
↑ 只有 acquire 成功的线程是活动的(深色表示),其他的线程都是等待状态(浅色)
ObjectMonitor 的 C++代码实现参考 @ref: synchronized 实现原理 | 小米信息部技术团队
➤ 回到开头的例子:
public class Bank { |
- 执行到(1)进入 sync 代码块,当前线程 A 获得了对象锁;
- 执行(2)wait 后,线程会释放锁,进入 waitSet 队列,并调用 park 让出 CPU
- 另一个线程 B 再尝试获取锁,如果成功且调用了 (3)notifyAll,A 线程被唤醒,然后尝试抢锁;
- 假设有线程 C 尝试获取锁失败,C 线程会 park,然后进入 cxq 队列,当有线程释放锁时,会尝试从 cxq 中删除线程 C 的节点,然后把 C 加入 entrySet,C 再尝试抢锁
需要注意的:
- 调用 wait 后,线程会放弃锁,其他线程可以尝试抢锁;
- waitSet 是调用 wait 的线程的队列,cxq 是抢锁失败的线程的队列
- notify:先 wait 的线程,先被唤醒;
- notifyAll:最后 wait 的线程,先被唤醒;
- wait 可以响应 InterruptedException 异常
@ref:: http://lovestblog.cn/blog/2016/03/27/object-wait-notify/
对象锁的优化-消除&粗化
锁粗化:如果在代码中,连续对同一个对象加锁,JIT(不确定)会把这块代码加入到一个同步块中;
锁消除:是虚拟机另外一种锁的优化,这种优化更彻底,Java 虚拟机在 JIT 编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下 StringBuffer 的 append 是一个同步方法,但是在 add 方法中的 StringBuffer 属于一个局部变量,并且不会被其他线程所使用,因此 StringBuffer 不可能存在共享资源竞争的情景,JVM 会自动将其锁消除。
// 参考自 https://blog.csdn.net/javazejian/article/details/72828483 |
需要注意的是,锁操作的 happens-before 规则的关键字是同一把锁。也就意味着,如果编译器能够(通过逃逸分析)证明某把锁仅被同一线程持有,那么它可以移除相应的加锁解锁操作。因此也就不再强制刷新缓存。举个例子,即时编译后的 synchronized (new Object()) {},可能等同于空操作,而不会强制刷新缓存。