Java锁优化
HostSpot虚拟机在 JDK 6 中实现了大量的锁优化技术,比如说适应性自旋,锁消除,锁膨胀,轻量级锁,偏向锁等。
自旋锁与自适应自旋
自旋锁
由于Java线程设计的原因,把一个线程挂机或者恢复都需要由用户态切换到核心态,这些都会给Java虚拟机带来很大的压力。但是由于现在用户的cpu都是多核的,可以让多个任务并行运行,所有很多共享数据锁的持有时间是很短的,如果因为这么短的时间就让一个线程挂起再恢复,很不值当。可以让一个线程等一小会儿,但不放弃处理时间,看看持有锁的线程是否很快会释放。而等的这个过程,我们只需要让线程忙循环(自旋),这就是自旋锁。
自旋锁并不能代替阻塞。如果持有锁的时间很短,那么自旋锁效率就很高,如果持有很长,自旋的线程占用着cpu却没有做有用的事,导致资源浪费。所以自旋锁一般设置循环次数,如果超过还没获取到锁,就挂起。
自适应自旋
JDK6中引入了自适应自旋,这时候自旋的次数不再是固定的了,改变为由上一次获取锁的时间以及锁持有者的状态来决定的。
如果在同一个对象上,自旋等待刚刚获取到锁,并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋也会成功,而且允许这次自旋的持续时间长一点。
另一方面,如果获取的较少,那么可能会直接掠过。
消除锁
指的是虚拟机即时编译器在运行时,对那些检测到不可能存在共享数据竞争的锁进行消除。主要判定依据来源于逃逸分析的数据支持。
如果一段代码,在堆上的所有数据都不会逃逸出去被其他线程访问到,那么就可以把它们当作栈上的数据对待。
注意,这里的加的锁可能并不是程序员自己加的,可能是调用某些方法,而这些方法内部有同步块,而虚拟机会对这些方法内部的同步块所加的锁进行消除。
比如如下代码:
1 | public String add(String str1, String str2) { |
StringBuilder的append方法内有同步块,所以会导致加锁,但是经过判定不需要这把锁,就会进行消除。因为这里没有其他线程会往该对象中添加数据。
锁粗化
如果加锁释放锁的动作频繁执行,比如说在一个循环中,那么会影响性能,这时候就不能按照把锁的粒度尽可能缩小的原则。此时就可以在循环前加锁,循环结束后释放锁,虽然增大了锁的粒度,但是某些场景可以提高性能。
像上面的那个append操作,可以在第一个append之前加锁,然后最后一次append结束释放锁,这样就避免了一直加锁,释放锁。
轻量级锁
这个轻量级是相对于使用操作系统互斥量来实现的传统锁相比。它并不能代替传统的重量级锁,它的设计目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量造成的性能消耗。
HotSpot虚拟机的头部分为两部分,第一部分用于存储对象自身的运行时数据,例如哈希码,GC的分代年龄等,这部分被称为Mark Word。这部分对于轻量级锁和偏向锁实现很关键。另一部分用于存储指向方法区对象类型数据的指针。
Mark Word这部分有两比特空间用于存储锁的标记为。对象除了未被锁定的状态外,还会有轻量级锁定,重量级锁定,GC标记,可偏向等不同的状态。
在代码即将进入同步块的时候,如果此同步对象没有被锁定,虚拟机会在当前线程的栈帧中建立一个锁记录(Lock Record)的空间,用于存储对象目前Mark Word的拷贝(这一步意思是在当前线程中,存储一份该对象的Mark Word的拷贝)。然后虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向锁记录的指针(将对象的Mark Word更新为指向线程的锁记录)。如果更新成功,即代表该线程拥有了该对象的锁,并且将Mark Word的锁标记位改为轻量级锁。
如果这次更新失败,说明至少有一个线程与当前线程竞争获取该对象的锁。此时,虚拟机会先检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明已经获取到该对象的锁,直接执行就可以了,否则说明该锁被其他线程占用了。如果出现两条以上的线程争用同一个对象,那么就要膨胀为重量级锁。
它的解锁过程也是CAS操作实现的。如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前Mark Word和线程中复制的那一份拷贝替换回来。如果替换成功,则同步过程就成功了,如果替换失败,则说明有其他线程尝试获取过该锁,就要在释放锁的同时,唤醒被挂起的线程。
偏向锁
偏向锁的目的是消除在无竞争情况下的同步原语,进一步提高性能。轻量级锁是在无竞争情况下使用CAS操作区消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS都不需要。
偏向锁会偏向于第一个获取它的线程,如果接下来执行的过程中,该锁一直没有被其他线程获取,那么持有偏向锁的线程则不需要进行同步。
具体来说,偏向锁会在对象头中存储一个标识,用于标记是否开启偏向锁。当一个线程访问对象时,如果该对象没有被锁定,则该线程会将对象头中的标识设置为当前线程的ID,并将偏向锁标识设置为1。之后,该线程访问该对象时,会直接进入偏向锁模式,无需再次加锁,从而提高了程序的性能。
当其他线程访问该对象时,会发现该对象已经被标记为偏向锁,并且偏向锁标识为1,此时会检查该对象的偏向线程ID是否为当前线程的ID,如果是,则直接进入偏向模式,否则表示有竞争发生,会将偏向锁升级为轻量级锁或重量级锁。
参考
《深入理解Java虚拟机》