1 Java对象头信息
Java对象在JVM中的结构如下:
java对象包括:
-
Mark Word(存储对象的hashCode或者锁信息)
-
Class Pointer(存储对象所属Class对象的指针)
-
length (只有数组对象才有这个字段)
-
Instance Data/Array Data (存储实际数据)
-
Padding (对齐缓存,填充对象被8整除的最小byte)
对象都在32/64位机器中每个部分分别是32/64位,Class Pointer在64位机器默认开启指针压缩,只占用32位。
2 对象加锁过程
对象加锁使用的是Mark Word字段,如下是32位的Mark Word
通过synchronize
关键字给对象加锁的过程如下:
-
无锁:对象刚创建,未被加锁;
-
偏向锁:Mark Word存储线程ID(默认开启)
-
轻量级锁(自旋锁):Mark Word 存储现在栈地址
-
重量级锁:调用操作系统的metux(已经不再JVM层面,发生了系统调用)
3 偏向锁
JVM引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID
的时候依赖一次CAS原子指令(一旦出现多线程竞争的情况就必须撤销偏向锁)。
3.1偏向锁获取过程
-
访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
-
如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
-
如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
-
如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
-
执行同步代码。
3.2 偏向锁的释放
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
4 轻量级锁
轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
4.1 轻量级锁的加锁过程
-
代码进入同步块的时候,如果同步对象锁状态为无锁状态,虚拟机将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间;
-
拷贝对象头中的Mark Word复制到锁记录中。
-
虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5);
-
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态;
-
如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,当前线程便尝试使用自旋来获取锁,自旋失败后,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
4.2 轻量级锁的解锁过程
- 通过CAS操作尝试把线程中复制的 Mark Word 对象替换当前的 Mark Word。
- 如果替换成功,整个同步过程就完成了。
- 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
5 重量级锁
synchronize的实现过程:
-
java语言层级:synchronize关键字实现加锁
-
字节码层级:moniterenter,moniterexit字节码实现加锁
-
JVM层级:偏向锁->轻量级锁->重量级锁(已经不在JVM,调用的操作系统的互斥量)
-
操作系统层级:申请操作系统的metux互斥锁实现加锁,会阻塞线程
-
硬件层级:通过 lock comxchg(CMS)实现获取锁资源,Lock前缀指令保障一致性
注意:lock前缀指令的功能:Synchronize, volatile,CMS都是使用这个实现
- 锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存(原子性保障)
- lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据(可见性保障)
- 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序(有序性保障)
当锁膨胀成重量级锁的时候,在JVM中当前锁对象关联的ObjectMonitor对象。
ObjectMonitor对象的数据结构如下:
-
WaitSet:存放被
wait()
方法waiting的线程 -
EntryList:存放获取不到锁被blocking的线程
-
OwnerThread:指向当前获取到锁的线程
-
recursions:记录锁重入的次数
EntryList是一个后进先出的双向链表,AQS(ReentrantLock)是一个先进先出的双向链表。
ObjectMoniter的流程:
-
拿到锁的线程修改ObjectMoniter的OwnerThread指向自己,recursions加一
-
未拿到锁的线程通过操作系统的阻塞,添加到EntryList队列中
-
当线程调用
wait()
方法时,线程进入Waitset集合,并释放锁 -
当调用
notify()
方法时,从Waitset集合取出,放入EntryList队尾
注意:
Synchronize只有一个WaitSet,AQS可以创建多个Condition队列(功能和Waitset类似)。
网友评论