以前Synchronised关键字加锁效率问题,经常受到吐槽。后来java的开发团队进行了优化,引入了偏向锁、自旋锁、轻量锁,性能有了很大的提升。下面我们来分析下这里面的过程和原理。
最初阶段,我们的代码段或者方法加syncronized关键字,如下:
syncronized(object) { // do something }
执行这段代码时,其实相当于,在这段代码前面加了monitorenter指令;后面加了monitorexit指令。如下:
monitorenter // do something monitorexit
这两指令都得和操作系统连接,相对来说是比较重的,比较耗时的。
但是,在代码里加synchronised 关键字时,只是全面考虑,很多情况只有一个线程在执行,如果在有这种情况下,就有些浪费了;另外,有时,即使多线程访问,他的等待时间是很短的,比monitorenter相关指令还轻,这也是可想办法优化的。
在叙述相关优化锁之前,我们先说下java 对象头部分的 Mark Word。在32位虚拟机上,他的长度是32位;在64位虚拟机上,他的长度是64位。
普通情况下,Mark Word的格式如下:
hash:25 ------------>| age:4 | biased_lock:1 lock:2
最后部分2个比特,是固定的,表示对象锁的状况
01 表示无锁,或者偏向锁; 00表示轻量级锁;10表示重量级锁
第三部分是标识是否偏向锁。当最后部分为01时,第三部分为0表示普通无锁对象,第三部分为1表示偏向锁;最后部分为其他值,第三部分不存在。
age表示分代年龄;hash表示对象hash值。
普通无锁:对象hash:25 ------------>| age:4 | 0 | 01
偏向锁:偏向锁的线程和时间戳:25 ------------>| age:4 | 1 | 01
轻量锁:指向栈中锁记录的指针:30 | 00
重量锁:指向互斥量(重量级锁)的指针:30 | 10
java1.6之后,对象有4中状态:无锁态,偏向锁态,轻量锁态,重量锁态。
java程序执行,没有遇到同步块时,都是无锁态。Mark Word如上普通无锁的情况。
遇到同步块之后,Mark Word从普通无锁变成偏向锁的情况,通过CAS加锁。如果是相同线程在次进入,不受影响。执行完成,CAS解锁,Mark Word又变成普通无锁的情况。
如果在偏向锁中,遇到锁竞争的情况。先线程停止,通过CAS锁升级到轻量锁,成功的一方继续执行,另一方开始自旋操作。待他完了继续执行。这里的自旋有个智能学习的策略。假设每个同步块的初始自旋次数是20次,A同步块的线程自旋时,每次很小的自旋次数就获得了执行时间,那后面他的自旋信用会很高,会超过20次往上升;B同步块的线程自旋时,每次都需要很大的自旋次数才能获得执行时间,后面就会把他的自旋次数从20往下降或者不给自旋机会。
当然,自旋失败后,锁得再次升级,通过CAS操作从轻量锁升级到重量锁,这就回到一开始的moniterenter指令和moniterexit指令了。
jvm还有其他一些锁的优化措施。
锁清除:在某些情况下,同步块中的代码不可能被其他线程访问修改的时候,这时候的锁是可以清除的。
锁粗化:在一些代码中,会频繁的加锁,加锁,加锁;当然,有时并不是程序员这么编写的,因为我们的jdk api中也会加些锁操作。我们得频繁的做互斥操作,可以把几个同步块合并成一个同步块来优化执行。