在C程序代码中咱们能够利用操做系统提供的互斥锁来实现同步块的互斥访问及线程的阻塞及唤醒等工做。然而在Java中除了提供Lock API外还在语法层面上提供了synchronized关键字来实现互斥同步原语。那么到底在JVM内部是怎么实现synchronized关键子的呢?html
1、synchronized的字节码表示:
java
在java语言中存在两种内建的synchronized语法:一、synchronized语句;二、synchronized方法。对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit字节码指令。而synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并无任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass作为锁对象。数据结构
2、JVM中锁的优化:多线程
简单来讲在JVM中monitorenter和monitorexit字节码依赖于底层的操做系统的Mutex Lock来实现的,可是因为使用Mutex Lock须要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是很是昂贵的;然而在现实中的大部分状况下,同步方法是运行在单线程环境(无锁竞争环境)若是每次都调用Mutex Lock那么将严重的影响程序的性能。不过在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减小锁操做的开销。oracle
锁粗化(Lock Coarsening):也就是减小没必要要的紧连在一块儿的unlock,lock操做,将多个连续的锁扩展成一个范围更大的锁。jvm
锁消除(Lock Elimination):经过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块之外被其余线程共享的数据的锁保护,经过逃逸分析也能够在线程本地Stack上进行对象空间的分配(同时还能够减小Heap上的垃圾收集开销)。oop
轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的状况下咱们程序中的大部分同步代码通常都处于无锁竞争状态(即单线程执行环境),在无锁竞争的状况下彻底能够避免调用操做系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只须要依靠一条CAS原子指令就能够完成锁的获取及释放。当存在锁竞争的状况下,执行CAS指令失败的线程将调用操做系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒(具体处理步骤下面详细讨论)。性能
偏向锁(Biased Locking):是为了在无锁竞争的状况下避免在锁获取过程当中执行没必要要的CAS原子指令,由于CAS原子指令虽然相对于重量级锁来讲开销比较小但仍是存在很是可观的本地延迟(可参考这篇文章)。优化
适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程当中执行CAS操做失败时,在进入与monitor相关联的操做系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)而后再次尝试,当尝试必定的次数后若是仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态。spa
3、对象头(Object Header):
在JVM中建立对象时会在对象前面加上两个字大小的对象头,在32位机器上一个字为32bit,根据不一样的状态位Mark World中存放不一样的内容,如上图所示在轻量级锁中,Mark Word被分红两部分,刚开始时LockWord为被设置为HashCode、最低三位表示LockWord所处的状态,初始状态为001表示无锁状态。Klass ptr指向Class字节码在虚拟机内部的对象表示的地址。Fields表示连续的对象实例字段。
4、Monitor Record:
Monitor Record是线程私有的数据结构,每个线程都有一个可用monitor record列表,同时还有一个全局的可用列表;那么这些monitor record有什么用呢?每个被锁住的对象都会和一个monitor record关联(对象头中的LockWord指向monitor record的起始地址,因为这个地址是8byte对齐的因此LockWord的最低三位能够用来做为状态位),同时monitor record中有一个Owner字段存放拥有该锁的线程的惟一标识,表示该锁被这个线程占用。以下图所示为Monitor Record的内部结构:
Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程惟一标识,当锁被释放时又设置为NULL;
EntryQ:关联一个系统互斥锁(semaphore),阻塞全部试图锁住monitor record失败的线程。
RcThis:表示blocked或waiting在该monitor record上的全部线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:用来避免没必要要的阻塞或等待线程唤醒,由于每一次只有一个线程可以成功拥有锁,若是每次前一个释放锁的线程唤醒全部正在阻塞或等待的线程,会引发没必要要的上下文切换(从阻塞到就绪而后由于竞争锁失败又被阻塞)从而致使性能严重降低。Candidate只有两种可能的值0表示没有须要唤醒的线程1表示要唤醒一个继任线程来竞争锁。
5、轻量级锁具体实现:
一个线程可以经过两种方式锁住一个对象:一、经过膨胀一个处于无锁状态(状态位001)的对象得到该对象的锁;二、对象已经处于膨胀状态(状态位00)但LockWord指向的monitor record的Owner字段为NULL,则能够直接经过CAS原子指令尝试将Owner设置为本身的标识来得到锁。
获取锁(monitorenter)的大概过程以下:
(1)当对象处于无锁状态时(RecordWord值为HashCode,状态位为001),线程首先从本身的可用moniter record列表中取得一个空闲的moniter record,初始Nest和Owner值分别被预先设置为1和该线程本身的标识,一旦monitor record准备好而后咱们经过CAS原子指令安装该monitor record的起始地址到对象头的LockWord字段来膨胀(原文为inflate,我以为之因此叫inflate主要是因为当对象被膨胀后扩展了对象的大小;为了空间效率,将monitor record结构从对象头中抽出去,当须要的时候才将该结构attach到对象上,可是和这篇Paper有点互相矛盾,两种实现方式稍微有点不一样)该对象,若是存在其余线程竞争锁的状况而调用CAS失败,则只须要简单的回到monitorenter从新开始获取锁的过程便可。
(2)对象已经被膨胀同时Owner中保存的线程标识为获取锁的线程本身,这就是重入(reentrant)锁的状况,只须要简单的将Nest加1便可。不须要任何原子操做,效率很是高。
(3)对象已膨胀但Owner的值为NULL,当一个锁上存在阻塞或等待的线程同时锁的前一个拥有者刚释放锁时会出现这种状态,此时多个线程经过CAS原子指令在多线程竞争状态下试图将Owner设置为本身的标识来得到锁,竞争失败的线程在则会进入到第四种状况(4)的执行路径。
(4)对象处于膨胀状态同时Owner不为NULL(被锁住),在调用操做系统的重量级的互斥锁以前先自旋必定的次数,当达到必定的次数时若是仍然没有成功得到锁,则开始准备进入阻塞状态,首先将rfThis的值原子性的加1,因为在加1的过程当中可能会被其余线程破坏Object和monitor record之间的关联,因此在原子性加1后须要再进行一次比较以确保LockWord的值没有被改变,当发现被改变后则要从新进行monitorenter过程。同时再一次观察Owner是否为NULL,若是是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。
释放锁(monitorexit)的大概过程以下:
(1)首先检查该对象是否处于膨胀状态而且该线程是这个锁的拥有者,若是发现不对则抛出异常;
(2)检查Nest字段是否大于1,若是大于1则简单的将Nest减1并继续拥有锁,若是等于1,则进入到第(3)步;
(3)检查rfThis是否大于0,设置Owner为NULL而后唤醒一个正在阻塞或等待的线程再一次试图获取锁,若是等于0则进入到第(4)步
(4)缩小(deflate)一个对象,经过将对象的LockWord置换回原来的HashCode值来解除和monitor record之间的关联来释放锁,同时将monitor record放回到线程是有的可用monitor record列表。
6、参考资料:
注:有理解错误之处欢迎指出,谢谢!