java多线程(5) - Java虚拟机中的锁优化技术以及几种锁的介绍

线程的五种状态:
thread

自旋锁

在程序中,Java虚拟机的开发工程师们在分析过大量数据后发现,共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。

如果物理机上有多个处理器,可以让多个线程同时执行的话,就可以让后来的线程“稍微(忙)等一下”,不会放弃处理器事件,看看持有锁的线程会不会很快释放锁。这个“稍微等一下”的过程就是自旋。

锁消除

在动态编译同步块的时候,JIT编译器可以借助一种叫逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。

如果同步块所使用的锁对象通过这种分析被证实只能被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。

比如以下代码:

public void f() {
    Object hollis = new Object();
    synchronized(hollis) {
        System.out.println(hollis);
    }
}

明显hollis的并不会被其他线程访问,所以JIT在编译时就会优化掉:

public void f() {
    Object hollis = new Object();
    System.out.println(hollis);
}

锁粗化

通常情况下,我们提倡尽量减小锁的粒度,把无关的准备工作放到锁外面,锁内部只处理和并发相关的内容,这样可以避免不必要的阻塞。

但是有些时候,在一段代码中连续对同一个对象反复加锁解锁,其实是相对耗费资源的,这时候可以恰当放宽加锁的范围,减少性能损耗。

JIT发现一系列连续的操作都对同一个对象反复加解锁,甚至加锁出现在循环体中时,会将加锁同步的方位扩散(粗化)到整个操作序列的外部。

如:

for(int i=0;i<100000;i++){  
    synchronized(this){  
        do();  
} 

会被粗化为:

synchronized(this){  
    for(int i=0;i<100000;i++){  
        do();  
} 

自适应自旋锁

自适应意味着自旋的时间不固定,而是由上一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定:

  • 如果在同一个锁对象上,自旋等待刚刚成功获得锁,并且持有锁的线程正在运行,那么虚拟机认为这次自旋也很可能再次成功,进而它允许自旋等待持续相对更长的时间,比如100个循环;
  • 相反,如果对某个锁,自旋很少成功获得,那么在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

轻量级锁

如果锁竞争激烈,我们不得不依赖重量级锁。如果完全没有实际的锁竞争,那么申请重量级锁时浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。

轻量级锁不需要申请互斥量。在代码进入同步块的时候,如果同步对象锁状态为无锁状态,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁。否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争,接下来膨胀为重量级锁。

lock、unlock和Mark Word之间的联系:
custom
图中提到了拷贝object mark word,由于脱离了原始mark word,官方将它冠以displaced前缀,即displaced mark word。
这个displaced mark word就是在CAS中作为compare的条件的。

拷贝mark word的原因是不想在lock和unlock这种底层操作上再加同步。

拷贝完object mark word后,JVM做了一步交换指针的操作,即流程中第一个橙色矩形框内容所述。
将object mark word里的轻量级指针指向lock record所在的stack指针,作用是让其他线程知道,该object monitor已被占用。
lock record里的owner指针指向object mark word的作用是为了在接下来的运行过程中,识别哪个对象呗锁定了。
custom--2-
custom--1-

最后一步unlock中,我们发现,JVM同样使用了CAS来验证object mark word在持有锁到释放锁之间,有无被其他线程访问。
如果其他线程在持有锁这段时间里,尝试获取过锁,则可能自身被挂起,而mark word的重量级锁指针也会被相应修改。
此时,unlock后就需要唤醒被挂起的线程。
如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

偏向锁

轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

在没有实际竞争的情况下,还能够针对部分场景持续优化。如果不仅仅没有实际竞争,自始至终使用锁的线程都只有一个,那么维护轻量级锁都是浪费的。

偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放都至少需要一次CAS,而偏向锁只有初始化时需要一次CAS。

“偏向”的意思是,假定将来只有第一个申请锁的线程会使用锁,因此只需要在Mark Word中CAS记录owner,如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁。否则,说明有其他线程竞争,膨胀为轻量级锁。

(步骤):

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
  2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
  5. 执行同步代码。

偏向锁的对象头Mark Word的结构:
b1221c308d2aaf13d0d677033ee406fc

偏向锁的撤销:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

总结

  • 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
  • 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
  • 重量级锁:有实际竞争,且锁竞争时间长。

重量级锁、轻量级锁和偏向锁的转换:
820406-20160424163618101-624122079

膨胀过程:
--------

comments powered by Disqus