Synchronized 中的重量级锁使用了 monitor,了解 monitor 和 对象头,是了解 Synchronized 的关键。
1. 对象头
JVM 虚拟机使用一种叫做「普通对象指针」(ordinary object pointer,OOP)的数据结构来描述 Java 对象实例信息。OOP 一般由对象头(Object Header) 、实例数据(Instance Data) 和 对齐填充(Padding) 三部分组成。
以32位虚拟机为例,普通对象的 Object Header 占8个字节共64位。实例数据(Instance Data) 存储的是对象的实际数据,因此大小与实际数据大小一致。对齐填充(Padding) 是可选项,用于将内存对齐为8字节的整数倍。
普通对象的 Object Header
以32位虚拟机为例,普通对象的 Object Header 占用 8 字节。其中4个字节是 Mark Word。另外4个字节是 Klass Word,用于指向对象的类型。
数组对象的 Object Header
以32位虚拟机为例,数组对象的 Object Header 占用 12 字节,多出的4字节用来表示数组的长度。
2. Mark Word
Mark Word 是 Object Header 中的一部分,以32位虚拟机为例,Mark Word 占用4个字节(32位)。值得注意的是,Java 对象在不同的状态下,Mark Word 存储的值完全不同。
初始状态 Mark Word 存储结构如下(对应上图中的第二行)
对象的hashCode (25 bits) 分代年龄 age (4 bits) 是否是偏向锁 biased_lock (1 bit) 锁标志位(2 bits) 对象状态(State) 25 4 0 01(未与任何锁关联) Normal(未加锁) 偏向锁状态 Mark Word 存储结构如下(对应上图中的第三行)
线程ID thread epoch 分代年龄 age (4 bits) 是否是偏向锁 biased_lock (1 bit) 锁标志位(2 bits) 对象状态(State) 25 2 4 1 01(未与任何锁关联) Biased 轻量级锁状态 Mark Word 存储结构如下(对应上图中的第四行)
栈帧中锁记录的地址 ptr_to_lock_record (30 bits) 锁标志位(2 bits) 对象状态(State) 30 00(轻量级锁) Lightweight Locked(轻量级锁) 重量级锁状态 Mark Word 存储结构如下(对应上图中的第五行)
ptr_to_heavyweight_monitor
:就是指向 monitor 的指针。栈帧中锁记录的地址 ptr_to_heavyweight_monitor (30 bits) 锁标志位(2 bits) 对象状态(State) 30 10 Heavyweight Locked
3. monitor
monitor 是操作系统提出来的一种高级原语,在操作系统层面称为「管程」,但其具体的实现模式,不同的编程语言都有可能不一样。它是 synchronized 实现的关键。monitor 从 Java 层面经常被称为「监视器锁」。
上面铺垫了这么多东西,其实就是为了明确 monitor 和 Object Header 中 Mark Word 的关系。每个 Java 对象都可以关联一个 monitor 对象,通过上面的内容可以看出来,只有在重量级锁的情况下 Object Header 中 Mark Word 才会设置指向 monitor 对象的指针。
monitor 内部由三部分组成:
- Owner:用于记录当前 monitor 的所属线程。
- EntryList:是一个链表结构,用于记录阻塞在当前锁对象上的线程。
- WaitSet:用于记录获取锁之后进入Waiting状态的线程。
当对象锁发生锁竞争时,在同一时刻只有一个线程能够获取到锁,其他线程会进入阻塞(BLOCKED)状态,此时这些被阻塞的线程就会进入EntryList 中等待锁持有者释放锁后被唤醒,再次参与锁竞争(非公平)。
如下示例:
1 | synchronized (obj) { |
刚开始,monitor 中的 Owner 为 null。
当 Thread1 执行
synchronized (obj)
就会将 monitor 的 Owner(所有者) 设置为 Thread1,monitor 中只能有一个 Owner。在 Thread1 上锁的过程中,如果 Thread2、Thread3、Thread4 也来执行
synchronized (obj)
,它们就会根据 obj 对象头中 Mark Wrod 找到 monitor 对象,发现 Owner 已经被 Thread1 持有,就会进入 monitor 对象中 EntryList(阻塞等待队列) 中,阻塞等待。当 Thread1 执行完同步代码块的内容,然后唤醒 EntryList(阻塞等待队列) 中等待的线程重新竞争锁,竞争是非公平的。竞争成功的线程成为锁拥有者,失败的线程继续在阻塞队列中阻塞。
当对象获取到锁之后,由于某些资源并未准备完成,需要等待其他线程去准备资源,此时线程会通过 wait/notify 等方法进入等待/通知模式,在这种情况下线程释放锁之后会进入 WaitSet,当其他线程准备好资源之后会通知 WaitSet 中等待的线程,WaitSet 中的线程会进入到 EntryList 中,重新参与锁竞争。
4. monitorente && monitorexit
知道了 monitor 是什么,也知道了 Java 对象与 monitor 之间的关系,但是还有一层疑问:程序在运行过程中是如何知道要给 Java 对象去关联一个 monitor 呢?
Java 的源代码在编译器编译之后生成的 Class 文件中存储的是字节码指令,程序执行本质上是一条条指令按照既定顺序的流水线工作,编译器在编译成 Java 字节码时做了记号,这个记号就是 monitorente
/monitorexit
。
monitorenter
字节码指令理解为「加锁」,monitorexit
字节码指令理解为「释放锁」。
看一段简单的 synchronized 代码块:
1 | static final Object LOCK = new Object(); |
将上述代码反编译成字节码后如下:
1 | Constant pool: |
可以将上述字节码代码分成两部分。0 ~ 16 是临界区中的代码逻辑,按部就班的,注释比较清楚不再赘述。19 ~ 23 是用来异常处理的,如果临界区代码中抛出异常,这段代码会释放锁,防止出现死锁。
上面反编译字节码代码中有一个
Exception table
,它会监测指定范围,如果范围内出现异常,就会进行一些额外的处理。以上面代码为例,Exception table
监测的范围如,6 ~ 16,即:counter++;
也就是同步代码块内的逻辑,如果出现异常会跳转到第19。
5. 扩展
5.1. monitorenter 字节码指令
当一个线程进入同步代码块时,它使用 monitorenter
字节码指令请求进入;如果当前对象的监视器计数器为0,则它会被准许进入。
如果当前对象的监视器计数器为1,则判断持有当前监视器的线程是否为自己;如果是,则进入,并增加计数(注意!支持重入)。
否则进行等待(阻塞),直到对象的监视器计数器为0,才会被允计进入同步块。
The objectref must be of type
reference
.Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
- If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
- If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
- If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
阅读自:The Java Virtual Machine Instruction Set (Java虚拟机规范) Chapter 6.5.monitorenter
5.2. monitorexit 字节码指令
当前线程退出同步代码块时,需使用 monitorexit
字节码指令声明退出,该字节码指令会减少当前对象的监视器计数。
如果当前对象监视器计数变为0,那当前线程不再是监视器的所有者;其他正在阻塞的线程允许尝试进入监视器。
- 多个
monitorexit
字节码指令可以与monitorenter
字节码指令一起使用。 - 同步代码块正常执行完成时,退出对象监视器是由Java虚拟机的
return
字节码指令处理。 - 同步代码块执行时意外中断,退出对象监视器由Java虚拟机的
athrow
字节码指令隐式处理。 - 同步代码块执行中抛出异常,退出对象监视器由Java虚拟机的异常处理机制来完成。
保证同步代码块不论是正常执行完成,还是突然中断或是异常结束,都能释放锁。
The objectref must be of type
reference
.The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
One or more monitorexit instructions may be used with a monitorenter instruction to implement a
synchronized
statement in the Java programming language . The monitorenter and monitorexit instructions are not used in the implementation ofsynchronized
methods, although they can be used to provide equivalent locking semantics.The Java Virtual Machine supports exceptions thrown within
synchronized
methods andsynchronized
statements differently:
- Monitor exit on normal
synchronized
method completion is handled by the Java Virtual Machine’s return instructions. Monitor exit on abruptsynchronized
method completion is handled implicitly by the Java Virtual Machine’s athrow instruction.- When an exception is thrown from within a
synchronized
statement, exit from the monitor entered prior to the execution of thesynchronized
statement is achieved using the Java Virtual Machine’s exception handling mechanism…
阅读自:The Java Virtual Machine Instruction Set (Java虚拟机规范) Chapter 6.5.monitorexit