定义
synchronized
synchronized是Java提供的内置锁,属于悲观锁、互斥锁、可重入锁、非公平锁。
使用方式
| 修饰对象 | 锁对象 |
|---|---|
| 实例方法 | 实例对象(this) |
| 静态方法 | 类的Class对象 |
| 代码块 | 括号中的对象 |
原理
字节码
修饰代码块
public void foo() {
synchronized(this) {
// do something...
}
}JVM会在同步代码块的入口插入1个monitorenter指令,并在正常退出路径和异常路径(通过Exception Table捕获所有异常)分别插入一个monitorexit指令。保证了锁一定会释放,避免死锁。
修饰方法
public synchronized void foo() {
// do something...
}JVM会将方法的access_flags中的ACC_SYNCHRONIZED标志位设为1。调用该方法时,会先自动获取对象的Monitor进行加锁,执行完毕后隐式释放锁。
Java对象头
对象在堆中的结构:
- 对象头
- Mark Word:根据当前对象的锁状态存储不同内容,64位系统下占8字节:
| 锁状态 | 内容 | 偏向锁标记 (1 bit) | 锁标志位 (2 bit) |
|---|---|---|---|
| 无锁 | HashCode (31bit) + 分代年龄 (4bit) | 0 | 01 |
| 偏向锁 | 偏向线程 ID (54bit) + Epoch(2bit) + 分代年龄(4bit) | 1 | 01 |
| 轻量级锁 | 指向线程栈中 Lock Record(锁记录) 的指针 | 无 | 00 |
| 重量级锁 | 指向堆外 C++ ObjectMonitor 对象的指针 | 无 | 10 |
| GC 标记 | CMS/G1 垃圾收集器标记 | 无 | 11 |
2. **Klass Word**:指向元空间中所属类的元数据。
3. **数组长度**:仅当对象为数组时存在。
2. 实例数据
3. 对齐填充
锁升级
JDK1.6之前,synchronized只有重量级锁,每次都会发生用户态和内核态的切换,性能开销极大。
JDK1.6后引入了锁分级,锁升级路径通常为:无锁⟶偏向锁⟶轻量级锁⟶重量级锁。
偏向锁在JDK15开始默认禁用,JDK18被移除。
偏向锁
设计意图:多数情况下,锁不存在多线程竞争,总是由一个线程获得。
加锁:第一次获取锁时,通过CAS将线程ID写入Mark Word。此后进入临界区,只需要对比Mark Word的线程ID是否相等。
撤销:当出现另一个线程竞争时,偏向锁被撤销。撤销必须等待到达全局安全点并暂停偏向线程,检查偏向线程是否存活:
- 若线程已经消亡或退出临界区:锁恢复为无锁状态,随后升级为轻量级锁。
- 若线程未退出临界区:直接升级为轻量级锁。
批量重偏向:当同一个类的实例在多线程下被撤销偏向达到20次,后续跳过撤销偏向,直接CAS将偏向线程改为自己的线程。
当锁对象处于偏向锁状态调用.hashCode()时,因为此时Mark Word中没有存储hashCode的字段,偏向锁会直接升级为重量级锁,将hashCode存储在_header中。
轻量级锁
设计意图:多线程在不同的时间交替获取锁,并发竞争较低。
加锁:
- 进入同步块前,JVM在当前线程栈帧中创建一个Lock Record(锁记录)空间。
- 将对象当前的Mark Word复制到锁记录中。
- 线程通过一次CAS将对象头的Mark Word的指针指向当前线程的锁记录。
- 成功:当前线程获得锁,锁标志位变为00。
- 失败:其他线程持有锁,存在竞争,线程进入自旋。
解锁:
- 使用CAS将栈帧中的Displaced Mark Word替换回对象头。
- 成功:没有并发竞争,顺利恢复无锁状态。
- 失败:说明在持有期间,有其他自旋超时的线程将锁升级为了重量级锁。此时释放锁需要按重量级锁的退出流程,唤醒
_cxq / _EntryList中的等待线程。
自适应自旋:JDK1.6引入。自旋次数不再固定,而是由上一次在同一个锁上的自旋成功率和锁持有者的状态决定。
重量级锁
设计意图:轻量级锁自旋超过一定限度或自旋期间第三个线程参与竞争时升级为重量级锁。
加锁:Mark Word的指针被修改为指向堆外C++的ObjectMonitor结构体,锁标志位变为10,未获取到锁的线程会被系统内核挂起,进入_cxq队列等待。
ObjectMonitor:
_owner:指向当前持有锁的线程,NULL表示未占用。_recursions:重入次数。_cxq / _EntryList:处于等待锁状态的线程队列。_WaitSet:调用了 wait() 处于等待状态的线程队列。_header:存储对象的hashCode。
锁优化
- 锁消除:JIT编译期通过逃逸分析,如果一个带锁的对象绝对不会逃逸出当前方法(如StringBuffer.append()),编译时会直接消除monitorenter和monitorexit。
- 锁粗化:如果一段代码频繁对同一个对象加锁、解锁(如循环),JIT会扩大锁的范围,减少加锁解锁的次数。