Java 对象头
以 32 位虚拟机为例
普通对象
所以以 Integer 和 int 为例子
- Integer 8字节对象头 + 4字节 int 值,所以大小是 int 的 3 倍
- int 4字节 int 值
数组对象
如 Student[] s = new Student[8],还包括数组长度 length
其中 markword 结构为
Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中:
- 对象未被锁定的状态下, Mark Word的32个比特空间里的25个比特将用于存储对象哈希码 hashcode,4个比特用于存储对象分代年龄 age,2 个比特用于存储锁标志位,还有1个比特固定为0(这表示未进入偏向模式)。
- 进入 Monitor 之后,即重量级锁状态 10 后,ptr_to_heavyweigth_monitor 指向 monitor 对象(monitor 对象是由操作系统提供的?)。hashcode,age 这些会被被指向 monitor 的指针覆盖,它们会被暂存在 monitor 中,monitor 结束后会把它们还原。
对象除了未被锁定的正常状态外,还有轻量级锁定、重量级锁定、GC标记、可偏向等几种不同状态。
64 位虚拟机 Mark Word
Monitor 对象(重量级锁)
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 对象结构如下
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner
- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析
注意:
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
sychronized 字节码
static final Object lock = new Object(); static int counter = 0; public static void main(String[] args) { synchronized (lock) { counter++; } }
上面这段代码对应的字节码为
- 同步代码:通过 moniterenter、moniterexit 关联到到一个monitor对象,进入时设置Owner为当前线程,计数+1、退出-1。除了正常出口的 monitorexit,还在异常处理代码里插入了 monitorexit。
- 实例方法:隐式调用moniterenter、moniterexit
- 静态方法:隐式调用moniterenter、moniterexit
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: getstatic #2 // <- lock引用 (synchronized开始) 3: dup 4: astore_1 // lock引用 -> 暂存到 slot 1 5: monitorenter // 加锁。将 lock对象 MarkWord 置为 Monitor 指针,覆盖掉原来的 hashcode,age(暂存到了 monitor 中) 6: getstatic #3 // <- i 9: iconst_1 // 准备常数 1 10: iadd // +1 11: putstatic #3 // -> i 14: aload_1 // <- lock引用 15: monitorexit // 解锁。将 lock对象 MarkWord 重置为 hashcode,age, 唤醒 EntryList 16: goto 24 19: astore_2 // 同步代码块发生异常,会走这一块进行异常处理。e -> slot 2 20: aload_1 // <- lock引用 21: monitorexit // 异常处理中进行解锁。将 lock对象 MarkWord 重置, 唤醒 EntryList 22: aload_2 // <- slot 2 (e) 23: athrow // throw e 24: return Exception table: from to target type 6 16 19 any 19 22 19 any LineNumberTable: line 8: 0 line 9: 6 line 10: 14 line 11: 24 LocalVariableTable: Start Length Slot Name Signature 0 25 0 args [Ljava/lang/String; StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 19 locals = [ class "[Ljava/lang/String;", class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4
轻量级锁(Lock Record)
monitor 是操作系统提供的对象,每次使用它成本比较高,Java的线程是映射到操作系统的原生内核线程之上的,如果要阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这就不可避免地陷入用户态到核心态的转换中,进行这种状态转 换需要耗费很多的处理器时间。
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争,一个加锁解锁完了另一个才过来),那么可以 使用轻量级锁来优化。 轻量级锁对使用者是透明的,即语法仍然是 synchronized。意义在于 只有少量竞争时,不用创建 Monitor (操作系统互斥量,开销较大)。但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。
- 创建锁记录(Lock Record)对象:每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 MarkWord
- 判断 MarkWord 是否为无锁状态,标识位001;如果 MarkWord 处于无锁状态,将现在对象的无锁状态 MarkWord(存的是hashcode age)暂存到 Lock Record 的 Displaced Mark Word 中。
- 通过 CAS 尝试将 对象头的无锁 MarkWord 更新为指向 Lock Record 对象的指针
- 如果更新成功,表示竞争到锁, MarkWord 状态转为 00 (轻量级锁),然后执行同步代码。
- 如果更新失败:
- 其它线程竞争(MarkWord 指向其它线程):表明有多个线程竞争轻量级锁,轻量级锁需要膨胀升级为重量级锁。去 Monitor 的 EntryList 等待去了。
- 同一线程锁重入(MarkWord 指向当前线程):那么再添加一条 Lock Record 作为重入的计数(但是 Lock Record 的 Displaced Mark Word为 null)。
- 当退出 synchronized 代码块(解锁时)
- 如果有 Displaced Mark Word 为 null 的锁记录,表示有重入,这时重置取值为 null 的这个锁记录,表示重入计数减一
- 锁记录 Displaced Mark Word 不为 null,这时使用 cas 将锁记录中的 Dispalced Mark Word 即原来无锁状态的 Mark Word 恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
为什么JVM选择在线程栈中添加
Displaced Mark word
为null的Lock Record
来表示重入计数呢?首先锁重入次数是一定要记录下来的,因为每次解锁都需要对应一次加锁,解锁次数等于加锁次数时,该锁才真正的被释放,也就是在解锁时需要用到说锁重入次数的。
一个简单的方案是将锁重入次数记录在对象头的
mark word
中,但mark word
的大小是有限的,已经存放不下该信息了。另一个方案是只创建一个
Lock Record
并在其中记录重入次数,Hotspot没有这样做的原因我猜是考虑到效率有影响:每次重入获得锁都需要遍历该线程的栈找到对应的Lock Record
,然后修改它的值。所以最终Hotspot选择每次获得锁都添加一个
Lock Record
来表示锁的重入。
锁膨胀
轻量级锁时 Thread -1 尝试 CAS 将无锁的 MarkWord 更新为指向 Lock Record 对象的指针:如果更新失败,并且不是锁重入,即 MarkWord 指向的非当前线程。
说明是有其它线程为此对象加上了轻量级锁,发生了锁竞争。这时需要进行锁膨胀,将轻量级锁变为重量级锁。
锁膨胀流程
- 为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList BLOCKED
当 Thread-0 退出同步块解锁时,使用 cas 将 MarkWord 的值恢复给对象头(本应指向自己这个 Lock Record 但现在指向了 Monitor),失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
自旋优化
自选:让线程先不要进入阻塞,而是进行几次循环。因为阻塞意味着线程会发生一次上下文切换
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
自旋重试成功的情况
自旋重试成功的情况
偏向锁
参考:
黑马程序员:深入学习Java并发编程,JUC并发编程全套教程
有赞:Java锁与线程的那些事
标签:Monitor,对象,升级,Record,MarkWord,线程,Sychronized,优化,轻量级 From: https://www.cnblogs.com/suBlog/p/17595294.html