《并发编程》--19.虚拟机内的锁优化

来源:互联网 发布:知天气福建版 数值预报 编辑:程序博客网 时间:2024/05/18 08:35
首先要介绍下对象头,在JVM中,每个对象都有一个对象头。
Mark Word,对象头的标记,32位(32位系统)。
描述对象的hash、锁信息,垃圾回收标记,年龄
还会保存指向锁记录的指针,指向monitor的指针,偏向锁线程ID等。
简单来说,对象头就是要保存一些系统性的信息。

1 偏向锁

所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程 。
大部分情况是没有竞争的(某个同步块大多数情况都不会出现多线程同时竞争锁),所以可以通过偏向来提高性能。即在无竞争时,之前获得锁的线程再次获得锁时,会判断是否偏向锁指向我,那么该线程将不用再次获得锁,直接就可以进入同步块。
偏向锁的实施就是将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
当其他线程请求相同的锁时,偏向模式结束
JVM默认启用偏向锁 -XX:+UseBiasedLocking
在竞争激烈的场合,偏向锁会增加系统负担(每次都要加一次是否偏向的判断)

偏向锁的例子:

package test; import java.util.List;import java.util.Vector; public class Test {  public static List<Integer> numberList = new Vector<Integer>();   public static void main(String[] args) throws InterruptedException { long begin = System.currentTimeMillis(); int count = 0; int startnum = 0;  while (count < 10000000) { numberList.add(startnum); startnum += 2;count++; } long end = System.currentTimeMillis();  System.out.println(end - begin);    }}
Vector是一个线程安全的类,内部使用了锁机制。每次add都会进行锁请求。上述代码只有main一个线程再反复add请求锁。
使用如下的JVM参数来设置偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
BiasedLockingStartupDelay表示系统启动几秒钟后启用偏向锁。默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能。
由于这里为了测试偏向锁的性能,所以把延迟偏向锁的时间设置为0。
此时输出为9209
下面关闭偏向锁:
-XX:-UseBiasedLocking
输出为9627
一般在无竞争时,启用偏向锁性能会提高5%左右。

2 轻量级锁

Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意。
原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖操作系统互斥(mutex)来实现的。
互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作。
为了优化Java的Lock机制,从Java6开始引入了轻量级锁的概念。
轻量级锁(Lightweight Locking)本意是为了减少多线程进入互斥的几率,并不是要替代互斥。
它利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG),尝试在进入互斥前,进行补救。
如果偏向锁失败,那么系统会进行轻量级锁的操作。它存在的目的是尽可能不用动用操作系统层面的互斥,因为那个性能会比较差。因为JVM本身就是一个应用,所以希望在应用层面上就解决线程同步问题。
总结一下就是轻量级锁是一种快速的锁定方法,在进入互斥之前,使用CAS操作来尝试加锁,尽量不要用操作系统层面的互斥,提高了性能。
那么当偏向锁失败时,轻量级锁的步骤:
1.将对象头的Mark指针保存到锁对象中(这里的对象指的就是锁住的对象,比如synchronized (this){},this就是这里的对象)。
lock->set_displaced_header(mark);
2.将对象头设置为指向锁的指针(在线程栈空间中)。

if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(),mark)) {   TEVENT (slow_enter: release stacklock) ;  return ;  }

lock位于线程栈中。所以判断一个线程是否持有这把锁,只要判断这个对象头指向的空间是否在这个线程栈的地址空间当中。
如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁),就是操作系统层面的同步方法。在没有锁竞争的情况,轻量级锁减少传统锁使用OS互斥量产生的性能损耗。在竞争非常激烈时(轻量级锁总是失败),轻量级锁会多做很多额外操作,导致性能下降。

3 自旋锁

当竞争存在时,因为轻量级锁尝试失败,之后有可能会直接升级成重量级锁动用操作系统层面的互斥。也有可能再尝试一下自旋锁。
如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋),并且不停地尝试拿到这个锁(类似tryLock),当然循环的次数是有限制的,当循环次数达到以后,仍然升级成重量级锁。所以在每个线程对于锁的持有时间很少时,自旋锁能够尽量避免线程在OS层被挂起。
JDK1.6中-XX:+UseSpinning开启
JDK1.7中,去掉此参数,改为内置实现

如果同步块很长,自旋失败,会降低系统性能。如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能。

4 总结

上述的锁不是Java语言层面的锁优化方法,是内置在JVM当中的。
首先偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。
而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。
为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。
可见偏向锁,轻量级锁,自旋锁都是乐观锁。

原创粉丝点击