Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。值得注意的是,锁只能升级但不能降级。
1、偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈桢中的锁记录里存储锁偏向的线程ID,,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,偏向锁失效后,会升级为轻量级锁。
2、轻量级锁
轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!
如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!
3、自旋锁和自适应自旋
互斥同步是常见的一种并发正确性的保障手段,但是它对系统的并发性能有很大的影响,因为线程挂起和恢复的操作都需要操作系统从用户态转入内核态中完成(用户态转换到内核态会耗费时间)。
虚拟机的开发团队考虑到一般线程持有锁的时间都不是太长,这一点时间去挂起/恢复线程是不值得的。 所以,我们让后面请求锁的线程“稍等一下”,为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋锁。
自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,如果锁被占用的时间很长,也会因为做了很多无用功而带来性能上的浪费。因此自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。自旋次数的默认值是10次,用户可以修改--XX:PreBlockSpin来更改。
另外,在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁更加“聪明”:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定。
4、锁消除
锁消除指的是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
5、锁粗话
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,即使没有线程竞争,频繁的互斥同步操作会导致不必要的性能消耗。
如果虚拟机探测到有这样一串操作时都在对同一个对象加锁,将会把加锁同步的范围扩展(粗话)到整个操作序列的外部。