synchronized优化之锁粗化、锁消除、自旋优化
Jdk 1.5
以后对Synchronized
关键字做了各种的优化,经过优化后Synchronized
已经变得原来越快了,这也是为什么官方建议使用Synchronized
的原因,具体的优化点如下。
- 锁粗化
- 锁消除
- 自旋优化
锁粗化
互斥的临界区范围应该尽可能小,这样做的目的是为了使同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,锁粗化就是将「多个连续的加锁、解锁操作连接在一起」,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
J V M
会检测到一连串的操作都对同一个对象加锁(for
循环10000
次执行j++
,没有锁粗化就要进行10000
次加锁/解锁),此时J V M
就会将加锁的范围粗化到这一连串操作的外部(比如for
循环体外),使得这一连串操作只需要加一次锁即可。
锁消除
Java
虚拟机在JIT
编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析(对象在函数中被使用,也可能被外部函数所引用,称为函数逃逸),去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的时间消耗。
代码中使用Object
作为锁,但是Object
对象的生命周期只在incrFour()
函数中,并不会被其他线程所访问到,所以在J I T
编译阶段就会被优化掉(此处的Object
属于没有逃逸的对象)。
自旋优化
Synchronized自旋优化发生在轻量级锁产生竞争后,获取失败的线程将锁膨胀为重量级锁的过程中。失败的线程不会立刻阻塞,而是先尝试适应性自旋,等待所有者释放锁,当到达临界值后再阻塞。
介绍
monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能带来了很大的压力。
为了让线程等待,我们只需让线程执行一个循环,这项技术就是所谓的自旋锁。
自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。
因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户可以使用参数 -XX : PreBlockSpin 来 更改。
缺点
单核处理器上,不存在实际的并行,当前线程不阻塞自己的话,旧owner就不能执行,锁永远不会释放,此时不管自旋多久都是浪费;进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费。
自旋锁要占用 CPU,如果是计算密集型任务,这一优化通常得不偿失,减少锁的使用是更好的选择。
如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的CPU时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。
使用 -XX:-UseSpinning 参数关闭自旋锁优化;-XX:PreBlockSpin 参数修改默认的自旋次数。
适应性自旋锁
在JDK 6中引入了自适应的自旋锁。
自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
相反的,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。
自适应自旋解决的是 锁竞争时间不确定 的问题。
JVM 很难感知到确切的锁竞争时间,而交给用户分析就违反了 JVM 的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。