在jdk1.6以前,随着并发数提高,synchronized吞吐量下降严重,而ReentrantLock则比较稳定,如果说ReentrantLock性能较强,那么synchronized则有非常大的优化空间。而在JDK1.6发布后,两者性能基本持平。因此,性能问题不再是选择ReentrantLock的理由,虚拟机在未来性能改进中肯定也会更加偏向原生的synchronized,在synchronized能实现需求的情况下,推荐优先使用synchronized。
synchronized同步锁共有4中状态:
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
其随着竞争激烈程度的升级,synchronized锁开始膨胀:无锁-->偏向锁-->轻量级锁-->重量级锁。而在这个过程中,JDK1.6对锁进行了优化。
锁优化
自旋锁与自适应自旋
互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程都需要转入内核态中完成,给系统的并发能力带来很大压力。
实际操作发现,大部分场景在加锁后执行操作时间都很短,就开始释放锁。如果能让后面需要锁的线程稍微等一下,不去进行挂起操作,那它大概率很快就能得到锁,避免线程开销。这个等一下就叫自旋(执行一个忙循环)。
如果自旋时间过长,还没有获得锁,反而白白浪费时间片,因此默认的自旋会有一个自旋次数10.
JDK1.6引入了自适应自旋锁。其自旋次数根据上一次在同一个锁上的自旋时间来决定。如果在同一个锁,上一个线程刚刚获得过锁,并且正在运行中,则虚拟机会认为这次自旋大概率会成功,则会自动允许更长时间的自旋;如果某个锁,加锁时间过长,很少有线程可以通过自旋等到它,则以后获取这个锁的自旋过程可能被省略,以免浪费处理器资源。
锁消除
在一些虽然写了同步,但实际发现不可能存在竞争关系的场景,虚拟机会自动取消同步过程。
锁粗化
如果一系列操作对同一个对象反复的加锁解锁,甚至加锁出现在循环体内部,本没有线程竞争,导致了不必要的开销。
虚拟机会把加锁的同步范围扩展(粗化)到整个操作序列的外部,这样仅加锁一次即可。
膨胀过程
偏向锁
【只有一个线程竞争锁】
JDK1.6引入的一项锁优化,偏向于第一个获得它的线程。
多数场景下,锁没有竞争关系,总是由同一个线程多次获取,若该锁一直没有被其他线程获取,则持有偏向锁的线程将无需加锁。直到另一个线程到来,偏向锁开始膨胀。
如果程序中大部分情况有多个线程访问锁,那偏向模式比较多余。
轻量级锁
【多个线程交替竞争锁】
JDK1.6加入的新型锁机制。其使用CAS操作尝试更新对象,若更新成功了,这个线程就拥有了锁,此时就处于轻量级锁定状态;若更新失败了,开始自旋,自旋一定次数CAS操作依然没有成功,则判断拥有锁的是否为当前线程,如果是就直接进入同步代码块执行,如果不是,则说明两个线程在争夺锁。如果争夺锁的线程在2个以上,轻量级锁就要膨胀为重量级锁。
如果没有竞争,轻量级锁使用CAS避免了互斥开销,但如果存在多个线程的锁竞争,就要转换为重量级锁,因此在互斥的情况下还多了CAS操作,竞争激烈情况下,轻量级锁比重量级锁更慢。
重量级锁
【多个线程同时竞争锁】
总结
偏向锁、轻量级锁均为乐观锁,重量级锁为悲观锁。
- 最初加锁的对象没有线程访问,为无锁状态。当第一个线程来访问他,他偏向第一个线程,此时对象持偏向锁。
- 第二个线程来,发现该锁已经是偏向锁,检查原来拥有锁的线程是否存活,如果挂了,则先置为无锁状态,然后锁偏向新的线程,如果没挂,则升级(膨胀)偏向锁为轻量级锁
- 轻量级锁认为竞争存在,但竞争不激烈,只有两个线程,一般两个线程的竞争都会错开,或者在等待时简单自旋一下就能获得锁。但自旋超过一定次数,或者第三个线程来时,需要三个线程竞争锁,那么此时轻量级锁就会升级(膨胀 )为重量级锁。
- 锁的膨胀过程是不可逆的(偏向锁可以被重置为无锁状态)
- 偏向锁和轻量级锁在用户态维护,重量级锁需要切换到内核态维护(因为只有内核态才可以阻塞和唤醒线程)