synchronized实现原理
synchronized实现原理
1. Java对象头
在 JVM 中,不同变量类型的对象头大小是不同的。
- 数组类型,虚拟机用 3 个字宽存储对象头。
- 非数组类型,虚拟机用 2 个字宽存储对象头。
在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit。在 64 位虚拟机中,1 字宽等于 8 字节,即 64 bit。
Java 对象头由三部分组成:
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的 HashCode 或锁信息 |
32/64bit | Class Metadata Address | 指向存储对象类型数据的指针 |
32/64bit | Array length | 数组长度(如果当前对象是数组) |
Mark Word 中存储的数据会根据锁标志位的变化而变化,如下表所示:
64 位虚拟机中共 64bit:
2. synchronized实现原理
synchronized 实现同步的基础是 Java 中每个对象都可以作为锁,具体表现为以下三种形式:
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的 Class 对象。
- 对于同步方法块,锁是 synchronized 括号里配置的对象‘。
当线程试图访问同步代码块时,必须先获得锁,退出或抛出异常时必须释放锁。
synchronized 同步语句块的基础是 monitorenter 指令和 monitorexit 指令。monitorenter 在编译后插入同步代码块的开始位置, monitorexit 插入方法结束处和异常处。JVM保证了每个 monitorenter 都必须有 monitorexit 指令与之配对。线程执行到 monitorenter 指令时,将尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁
- 执行
monitorenter
时,会尝试获取对象的锁,如果锁计数器为0说明可以被获取,获取后将锁计数器置为1。 - 执行
monitorexit
时,会将锁计数器置为0,表明锁被释放。如果获取对象锁失败,那当前线程要阻塞等待,直到锁被另一个线程释放为止。
synchronized 同步方法的原理是在字节码中为方法添加一个ACC_SYNCHRONIZED
标识,指明该方法是一个同步方法。当看到这个标识后,自动在方法调用和退出时加上monitorenter 指令和 monitorexit 指令。
3. synchronized锁升级
在 Java1.6 中,为了减少锁的获得和释放带来的性能开销,引入了偏向锁和轻量级锁。于是锁一共有四种状态,由低到高依次是:
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级,提高了获得锁和释放锁的效率。
3.1 偏向锁
由于大多数情况下,锁不仅不存在多线程竞争,而且总是有一个线程多次获得。为了让线程获得锁的代价更低,Java 虚拟机引入了偏向锁。
获得偏向锁
- 当线程访问同步块并获取锁,会在对象头和栈帧的锁记录中存储锁偏向的线程 ID。
- 以后该进程进入和退出同步块时不使用 CAS 来加锁和解锁,而是简单测试当前对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。
- 如果测试成功,表示线程已经获得了锁。
- 如果测试失败,再测试一下 Mark Word 中偏向锁标识是否为 1。
- 如果没有设置,则使用 CAS 竞争锁。
- 如果设置了,则使用 CAS 将对象头的偏向锁指向当前线程。
撤销偏向锁
偏向锁的撤销,需要等待一个全局安全点,即这个时间点上没有正在执行的字节码。
- 首先暂停拥有偏向锁的线程,并检查这个线程是否活着。
- 如果线程处于不活动状态,则将对象头设置为无锁。
- 如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录。栈中的锁记录和对象头的 Mark Word 要么重新偏向于其他线程,要么恢复无锁或者标记对象不适合作为偏向锁。
- 最后唤醒暂停的线程。
禁用偏向锁
偏向锁时默认启用的,但是在应用程序启动几秒后在延迟激活。可以使用-XX:BiasedLockingStartupDelay=0
参数来关闭延迟。如果确定应用程序所有的锁通常情况下处于竞争状态,可以通过-XX:UseBiasedLocking=false
来禁用偏向锁。那么程序默认会进入轻量级锁状态。
3.2 轻量级锁
轻量级锁加锁
- 执行同步块前,在当前线程栈帧中创建用于存储锁记录的空间。
- 将对象头的 Mark Word 复制到锁记录中,称为 Displaced Mark Word。
- 尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。
- 如果成功,则当前线程获得锁。
- 如果失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁。
轻量级锁解锁
- 解锁时,使用原子的 CAS 操作将 Displaced Mark Word 替换回对象头。
- 如果成功,表示没有竞争发生。
- 如果失败,表示当前锁存在竞争,锁膨胀为重量级锁。
3.3 重量级锁
重量级锁就是利用对应的对象作为锁,获取不到就阻塞。
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或同步方法。没有获取到监视器的线程会被阻塞在入口处,进入 BLOCKED 状态。
当获得锁的线程释放了锁,还会唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
4. 锁的对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外消耗,和执行非同步方法相比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块的场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程,使用自旋会消耗 CPU。 | 追求响应时间且同步块执行速度非常快。 |
重量级锁 | 线程竞争不用自旋,不消耗 CPU | 线程阻塞,响应时间缓慢。 | 追求吞吐量且同步块执行时间较长。 |