一、前置知识 1 Java对象结构 每个Java对象都隐含一把锁,Java内置锁的很多重要信息都放在对象头部,对象头有三个字段:
Mark Word,用来存储自身运行时的数据,例如GC标志、哈希码、锁状态等; Class Pointer,用来存放类指针; Array Length,用来存放数组长度,可选。JVM要求对象起始地址是8字节的倍数,必要时填充字节。32位虚拟机中以上三个字段都是32位,64位虚拟机中如果堆小于32GB时,指针会压缩为32位。 2 锁状态 内置锁有4种状态:无锁、偏向锁、轻量级锁和重量级锁,分别适合没有竞争的场景、轻微竞争的场景、剧烈竞争的场景。锁状态会根据竞争情况由低到高升级,不可降级,目的是提高获取锁和释放锁的效率。
以64位来解释各字段含义:
lock:锁标志状态,占2比特; biased_lock:对象是否启用偏向锁; identity_hashcode:31位的哈希码采用延迟加载技术,当调用Object.hashCode()方法或者System.identityHashCode()方法计算对象的hashcode后,其结果写入到该对象头中;对象一旦生成hashcode就无法进入偏向锁状态,会膨胀为重量级锁; thread_id:持有偏向锁的线程ID; epoch:偏向时间戳; ptr_to_lock_record:占62位,在轻量级锁的状态下指向栈帧中锁记录的指针,持有锁的线程栈帧中会有一个lock record维护锁状态; ptr_to_heavyweight_monitor:占62位,在重量级锁状态下指向对象监视器的指针。 二、无锁 JVM中是小端模式,数据的低位字节放在内存地址的低位,001是无锁状态。
0x12345678
小端模式:0x78563412
大端模式:0x12345678
三、偏向锁 1 特点 偏向锁条件:只有单个线程,没有其它线程竞争。每次比较锁标志和thread_id,如果符合就表示内置锁偏向该线程,当前持有这把锁,不用去加锁和解锁,直接进入同步代码块,甚至都不用CAS,偏向锁在没有竞争时效率非常高。
Java15之后偏向锁因维护代价大被移除了。
偏向锁主要作用是消除没有竞争时同步原语,降低锁开销。
2 演示案例 线程1第一次对锁对象加锁时会用CAS更新Mark Word,设置为自己的thread_id,使用完锁后,Mark Word并没有变化,这意味着实际上没有解锁过程。在没有竞争时,偏向锁可以消除掉加锁解锁的同步原语逻辑,提高程序效率。
3 锁膨胀和撤销 当线程1使用完锁,线程2来加锁时,此时锁对象的Mark Word中thread_id指向线程1,线程2发现该锁并不是偏向自己,说明存在锁竞争,流程如下:
检查线程1是否存活,如果挂了,则将锁对象变为无锁状态,重新偏向线程2即可; 在安全点暂停线程1,遍历线程1的栈帧,检查是否存在锁记录lock record持有偏向锁。如果存在锁记录,就表明线程1还在使用偏向锁,发生锁竞争,那么清空锁记录为无锁状态,并修复锁记录指向的Mark Word,清除线程ID,最后将当前锁升级为轻量级锁并唤醒线程1。 四、轻量级锁 轻量级锁的目的是在竞争不激烈时,通过使用CAS来避免重量级锁,尽可能不使用操作系统提供的互斥锁,可以减少上下文切换。
1 核心原理 轻量级锁执行流程:
在抢锁线程进入临界区之前,如果内置锁没有被锁定,JVM首先在抢锁线程的栈帧中建立一个锁记录用于存放锁对象的Mark Word的拷贝; 然后抢锁线程使用CAS自旋操作尝试将内置锁对象的Mark Word的ptr_to_lock_record更新为抢锁线程栈帧中锁记录的地址,如果这个操作成功了,线程就拥有该锁; JVM将Mark Word中原来锁对象信息(哈希码等)保存在抢锁线程锁记录中的Displaced Mark Word字段,再将抢锁线程中锁记录的owner指向锁对象。 2 演示案例 当线程2竞争锁时,锁会升级为轻量级锁,线程1和线程2公平竞争,Mark Word中thread_id会更新为抢占锁的线程。
轻量级锁适用于持有锁时间短、竞争不激烈的情况,线程2会自旋一段时间,如果能够获取到锁就不需要进行内核态与用户态之间的切换进行阻塞线程,乐观锁的思想。如果没有获取到锁,则锁再次升级为重量级锁,而线程2会阻塞并进入到该对象的监视器对象的集合中等待被唤醒。
3 自适应自旋锁 自适应自旋锁等待线程空循环的自旋次数并非是固定的,而是动态地根据实际情况来调整,大概原理是:
如果抢锁线程在同一个锁对象上之前成功获取到锁,JVM就会认为这次自旋很有可能再次成功,自旋时间会长一些; 如果对于某个锁,抢锁线程很少成功,那么JVM将可能减少自旋时间,甚至省略自旋过程,避免浪费CPU资源; 总的来说,根据上一次自旋时间和结果来调整下一次自旋时间。
五、重量级锁 1 监视器 在JVM中每个对象都关联一个监视器对象,监视器和对象一起创建、销毁,重量级锁通过监视器保证了任何时间只允许一个线程进入临界区代码。
监视器是一个同步机制,主要特点:
同步:监视器所保护的临界区代码是互斥执行的,任何线程进入临界区都需要获得监视器的许可,离开时将许可归还; 协作:监视器提供阻塞队列、signal、wait机制,允许未能获取许可的线程进入阻塞队列等待唤醒。 监视器是由C++类ObjectMonitor实现,本质上是调用了操作系统的mutex机制。
加锁时采用pthread_mutex_lock系统调用实现,需要从用户态跳转到内核态。上下文切换需要保存断点、保存现场、跳转到内核中、恢复现场、恢复断点等上百条指令,消耗的时间可能比用户执行代码的时间还长。
ObjectMonitor中包含有:
EntryList:保存阻塞线程,满足所有条件,只差CPU,一般被notify/notifyAll唤醒的线程会从WaitSet进入EntryList,线程具备了抢夺监视器的资源,状态从WAITING变为BLOCKED; WaitSet:保存等待线程,线程还差某个条件没满足,等待被notify; Owner:当前获取监视器的线程,EntryList中线程抢夺到监视器后,状态就会从BLOCKED变为RUNNABLE 2 演示案例 如果临界区执行时间较长,线程2自旋后没有获取到锁,那么就会升级为重量级锁。线程2阻塞并进入到监视器对象中,Mark Word中前62位是monitor_ptr。
六、总结 总结一下synchronize执行过程,如下:
线程抢锁,JVM首先检测内置锁对象的Mark Word中biased_lock是否设置为1,lock是否为01,如果都满足,确认内置锁为偏向状态; 在内置锁对象确认为偏向状态后,JVM检查Mark Word中thread_id是否为抢锁线程,如果是就表示抢锁线程处于偏向锁状态,直接执行临界区代码,无需加锁解锁过程; 如果Mark Word中thread_id并未指向抢锁线程,就检查偏向线程是否存活,
a. 如果偏向线程已经挂了,抢锁线程就CAS更新Mark Word,成功就表示获取锁成功;
b. 如果还存活,就暂停该线程,并遍历其堆栈中是否存在锁记录,
i. 如果没有存在锁记录,就表示偏向线程当前并没有使用锁,抢锁线程CAS更新内置锁的Mark Word;
ii. 如果存在锁记录,就升级为轻量级锁; 如果抢锁线程没有获取到锁,竞争失败,就升级为轻量级锁。
a. JVM首先在抢锁线程的栈帧中建立一个锁记录用于存放锁对象的Mark Word的拷贝;
b. 然后抢锁线程使用CAS自旋操作尝试将内置锁对象的Mark Word的ptr_to_lock_record更新为抢锁线程栈帧中锁记录的地址,如果这个操作成功了,线程就拥有该锁;
c. JVM将Mark Word中原来锁对象信息(哈希码等)保存在抢锁线程锁记录中的Displaced Mark Word字段,再将抢锁线程中锁记录的owner指向锁对象。 如果抢锁线程没有获取到锁,锁升级为重量级锁,锁对象的Mark Word中ptr_to_heavyweight_monitor更新为监视器对象地址,抢锁线程阻塞并进入监视器等待队列中,等待被唤醒。