JDK1.6 对锁的优化: 偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化 等技术。
锁主要存在四中状态,依次是:
无锁状态
偏向锁状态
轻量级锁状态
重量级锁状态
锁可以升级不可降级,即 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的。 这种策略是为了提高获得锁和释放锁的效率。
偏向锁和轻量级锁的区别
偏向锁和轻量级锁都是为了:在无多线程竞争时,减少重量级锁中使用操作系统互斥量的性能消耗。轻量级锁在无竞争时使用 CAS 代替互斥量。而偏向锁则把整个同步都消除。轻量级锁是为了在线程交替执行同步块时,提高性能,而偏向锁则是在只有一个线程执行同步块时,提高性能。
偏向锁
偏向于第一个获得它的线程,如果接下来没有其他线程获取该锁,那么偏向锁的线程就不需要同步!
目的:为了在无多线程竞争的情况下,减少不必要的轻量级锁执行,因为偏向锁只需在置换线程ID 时依赖一次 CAS 指令(一旦出现多线程竞争,就必须撤销偏向锁)
轻量级锁的获取及释放要依赖多次 CAS指令
偏向锁的加锁
当一个线程访问同步块并获取锁时, 会在锁对象的对象头和栈帧中的锁记录中存储锁偏向的线程ID, 以后该线程进入和退出同步块不需要CAS来加锁和解锁, 只需简单测试对象头的MarkWord是否存储着指向当前线程的偏向锁, 如果测试成功, 则表示线程已经获得了锁; 如果测试, 则再测试MarkWord中偏向锁的标识是否设为1(表示当前是偏向锁), 如果没有设置, 则用CAS竞争锁, 如果设置了, 则用CAS将锁对象的对象头中的偏向锁指向当前线程.
偏向锁的撤销
偏向锁等到竞争出现才释放锁 即当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放. 偏向锁的撤销要等到全局安全点(在这个时间点上没有正在执行的字节码).
先会暂停持有偏向锁的线程, 然后检查持有偏向锁的线程是否存活, 如果不处于活动状态, 则把锁对象的对象头设为无锁状态,若如果线程存活, 则对象头中的MarkWord和栈中的锁记录要么重新偏向于其它线程,要么恢复到无锁状态, 最后唤醒暂停的线程(释放偏向锁的线程)
锁竞争激烈的场合,偏向锁失效,因为可能每次申请锁的线程都不同,注意:偏向锁失败后,会先升级为轻量级锁。
轻量级锁
偏向锁失败会尝试使用轻量级锁(1.6之后加入)。
轻量级锁:为了在无多线程竞争时,减少重量级锁中使用操作系统互斥量的性能消耗。轻量级锁的加锁和解锁用CAS。
轻量级锁提升性能的依据:“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是经验。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。如果存在锁竞争,轻量级锁比重量级锁更慢!除了互斥量开销外,还有CAS操作。如果锁竞争激烈,那么轻量级锁会很快变为为重量级锁!
自旋锁和自适应自旋
轻量级锁失败后,还会进行自旋锁的优化,
即让线程执行一个忙循环,就叫做自旋。“让后面请求获取锁的线程等待一会,看看持有锁的线程是否很快就会释放锁”。
因为线程持有锁的时间一般都不长,而线程的挂起/恢复要转入内核态中完成,耗费时间较大(用户态转换到内核态耗费时间)。
自旋锁在 JDK1.6 之前默认是关闭的,通过–XX:+UseSpinning来开启。
JDK1.6及以后,默认开启。注意:自旋等待不能完全替代阻塞,因为它还要占用cpu时间。
如果自旋超过了限定次数还没获得锁,则挂起线程。默认是10次,用户可以通过–XX:PreBlockSpin来更改。
JDK1.6 引入了自适应自旋锁。即自旋的时间不固定,由前一次同一个锁上的自旋时间和锁拥有者的状态决定。
锁消除
指虚拟机即时编译器运行时,**若检测到共享数据不可能存在竞争,则锁消除。**可以节省无意义的请求锁时间。
锁粗化
将多次高频的锁请求合并成一个锁请求,避免短时间内大量请求、释放锁的性能损耗。
如
锁粗化前:
public void doSomethingMethod(){
synchronized(lock){
//A 操作...
}
//存在少量中间代码,但很快执行完毕
synchronized(lock){
//B操作...
}
}
锁粗化后:
public void doSomethingMethod(){
synchronized(lock){
//A操作
//中间代码
//B操作
}
}
锁粗化前:
for(int i=0;i<size;i++){
synchronized(lock){
//操作
}
}
锁粗化后:
synchronized(lock){
for(int i=0;i<size;i++)
{
//操作...
}
}