1 Java 对象头
Java 对象头是 Synchronized 锁机制的基础,锁相关信息存储在 Java 对象头里面。对象头有两种形式,普通对象和数组对象,以32位JVM为例:
普通对象的对象头:
数组对象的对象头:
其中 Klass Word 是执行对象类型的指针,array length 是当对象为数组时用来存储数组长度的。MarkWord是锁机制的关键,单独说明下。
1.1 MarkWord
用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下(32位JVM):
2 锁机制实现原理
2.1 重量级锁
重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
2.1.1 重量级锁加锁过程
上图为线程获取锁的过程(注:图中多个线程为获取同一个monitor(锁),否则无竞争):
当多个线程同时访问一段同步代码时,首先会进入 Entry Set 竞争获取对象的monitor(锁)
当线程A获取到对象的monitor 后进入 The Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1
若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒
若线程调用 notify()/notifyAll() 方式时,将释放当前持有的monitor并复位变量,同时唤醒 WaitSet 中的等待线程(notify随机通知一个线程,notifyAll通知所有线程)重新竞争monitor
若线程执行完毕也将释放monitor并复位变量,以便其他线程获取monitor
由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。
2.2 轻量级锁
轻量级锁提升程序同步性能的依据是:对于绝大部分的锁,在整个同步周期内都是不存在竞争的(区别于偏向锁)。这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。
2.2.1 自旋锁与自适应自旋
Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的getter()和setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。
虚拟机的开发团队注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下“,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK1.6中已经变为默认开。自旋等待不能代替阻塞。自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会浪费处理器资源。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。
JDK1.6中引入自适应的自旋锁,自适应意味着自旋的时间不在固定。而是有虚拟机对程序锁的监控与预测来设置自旋的次数。
2.2.2 轻量级锁加锁过程
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
拷贝对象头中的Mark Word复制到锁记录(Lock Record)中
拷贝成功后,虚拟机将使用CAS操作尝试将锁对象的Mark Word更新为指向Lock Record的指针,并将线程栈帧中的Lock Record里的owner指针指向Object的 Mark Word。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
2.2.3 轻量级锁与膨胀流程
2.4 偏向锁
偏向锁是JDK6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当锁对象第一次被线程获取的时候,线程使用CAS操作把这个线程的ID记录在对象Mark Word之中,同时置偏向标志位1。以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的ID。如果测试成功,表示线程已经获得了锁。
当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定或轻量级锁定状态。
2.4.1 偏向锁获取流程
3. 小结
偏向锁、轻量级锁、重量级锁适用于不同的并发场景:
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
重量级锁:有实际竞争,且锁竞争时间长。
锁分配和膨胀流程全过程:
来源:oschina
链接:https://my.oschina.net/pengranxiang/blog/4279370