相关链接:
《synchronized 锁住的是谁?》
我们知道 synchronized 是重量级锁, 我们知道 synchronized 锁住的是一个对象上的 Monitor 对象, 我们也知道 synchronized 用于同步代码块时会执行 monitorenter 和 monitorexit 等.
上面几个问题仅仅是校招级.
那么 synchronized 为什么 "重" 呢? Monitor 对象从何而来呢? synchronized 用于实例方法或者静态方法又是怎么锁住的呢?
在《synchronized 锁住的是谁?》中我们明确了, synchronized 锁住的对象, 本文讲述 synchronized 凭什么锁得住.
首先我们需要知道的是在 Hotspot 虚拟机实现中, 对象实例在堆内存中结构分为 3 个部分: 对象头, 实例数据, 对其填充字节. 在 Java 中万物皆为对象. 就算一个 Java 类被编译称为 class 二进制文件在被加载到内存时, 它仍然会在堆内存中创建一个 Class 对象. 这也就解释了, 为什么 synchronized 能对类加锁(因为每个类在堆内存中有一个 Class 对象, 对于类 synchronized 锁的实际上是 Class 对象, 下文会继续解释).
在解释了 Java 中对象实例在 Hotspot 中的内存结构 (对象头, 实例数据, 对其填充字节) 后, synchronized 锁住的 Monitor 对象就存在于对象头之中. 对象头又分为: Mark Word, 指向类的指针, 数组长度(数组对象).
对象头在 Hotspot 虚拟机实现中, 分为 32 位和 64 位的实现, 实际上 Hotspot 源代码实现中的注释已经解释得非常清楚了(openjdk/hotspot/share/oops/markOop.hpp), 对象头的 Mark Word 位格式在 32 位机器中是 32 位长, 在 64 位机器中是 64 位长(采用 big endian , 低地址存放最高有效字节, 即低位在左, 高位再右).
32bit 位虚拟机 Mark Word | |||||
锁状态 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否是偏向锁 | 锁标志位 | ||
无锁状态 | 对象的 hashcode | 分代年龄 | 0 | 01 | |
偏向锁 | 线程 ID | 偏向时间戳 | 分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向重量级锁(Monitor)的指针 | 10 | |||
GC 标记 | 空 | 11 |
和 synchronized 相关的就是 Java 在 Hotspot 虚拟机实现中对象头中的 Mark Word.
在以前 (JDK5 之前),synchronized 被称为重量级锁是无可厚非的, 但在 JDK6 后, JVM 对其进行了一系列优化, 尽量使得 synchronized 不再那么重. 之所以 synchronized 重, 是因为它涉及到了操作系统用户态与核心态的转换, 下文再详细解释. 这里我们从最轻的偏向锁 -> 轻量级锁 ->重量级锁的过程, 注意他们只能升级加锁的强度, 不能降级.
偏向锁
上面提到了 JDK6 过后优化了 synchronized 的加锁过程, 尽量使得 synchronized 不再那么重. 偏向锁即是如此.
JVM 的研究者表明, 大多数情况下锁的竞争不是那么激励, 在不那么激励的时候如果通过获取 Monitor 来进行同步访问, 会造成线程在操作系统用户态和核心态的转换, 这会使得系统性能下降. 偏向锁表示, 当只有一个线程进入同步方法或同步代码块时, 并不会直接获取 Monitor 锁, 而是先判断对象头中 Mark Word 部分的锁标志位是否处于 "01", 如果处于 "01", 此时再判断线程 ID 是否是本线程 ID, 如果是则直接进入方法进行后续操作; 如果不是, 此时则通过 CAS(无锁机制竞争)如果竞争成功, 此时将线程 ID 设置为本线程 ID, 如果竞争失败, 说明造成了有了较为强烈的锁竞争, 偏向锁已不能满足, 此时偏向锁晋级为轻量级锁.
轻量级锁
当锁发生竞争时, 持有偏向锁的线程会撤销偏向锁, 转而晋级为轻量级锁(状态). 轻量级锁的核心是, 不让未获取锁的线程进入阻塞状态, 因为这会使得线程由用户态转为核心态, 这会造成很大的性能损失, 而是采用 "死循环" 的方式不断的获取锁, 这种采用 "死循环" 获取的锁的方式称为 -- 锁自旋. 它不会让线程陷入阻塞, 但同时仅适用于持有锁时间较短的场景. 那么轻量级锁升级为重量级锁的条件就是, 自旋等待的时间过长, 并且又有了新的线程来竞争.
重量级锁
这种锁, 就是地地道道原原本本 synchronized 的本意了. 线程会去抢夺对象上的一个互斥量(这个互斥量就是 Monitor), 每个对象都会有, 就算是类也有一个 Monitor 互斥量(因为类在堆内存中有一个 Class 对象). 当一个线程获取到对象的 Monitor 锁时, 其余线程会被阻塞挂起, 并且由用户态转为核心态.
上文提到在锁的竞争状态晋级为重量级锁时, Java 对象头中的 Mark Word 前 30 位存储的是 Monitor 对象的指针. Monitor 对象定义在 openjdk/hotspot/share/runtime/objectMonitor.hpp 中, 在 ObjectMonitor 中定义了: 计数器, 持有 Monitor 的线程, 处于 wait 状态的线程, 处于阻塞状态的线程等等.
synchronized 无论是普通实例还是同步代码块, 它所获取的锁是对象实例中的 Monitor 锁, 而对象的 Monitor 又是存在于 Java 对象头的 Mark Work 之中, 所以可以这么说, synchronized 获取的锁在 Java 对象头中. 对于普通实例或者静态方法, JVM 并没有显示的指令进入临界区, 而是在方法上标识了 "ACC_SYNCHRONIZED", 标识是 synchronized 同步方法, 方法内部都是临界区. 而对于同步代码块, 则在 synchronized 代码块开始执行了 monitorenter, 结束或者抛出异常时执行了 monitorexit 指令.
synchronized 凭借的就是 Monitor 锁住的对象, Monitor 又是借助于操作系统的 mutex lock, 之所以它重是因为它被挂起后线程会由用户态转换为内核态, 这个转换会带来性能损耗. JDK6 开始对其进行了优化, 提出了偏向锁和轻量级锁, 针对锁竞争较为激烈的场景不会直接去获取 Monitor 对象, 减少性能损耗. 因此在现如今的 synchronized 实现中, 它的性能劣势也已不再那么明显.
这是一个能给程序员加 buff 的公众号 (CoderBuff)
来源: https://www.cnblogs.com/yulinfeng/p/11048307.html