首先介绍发展史, synchronized
和Lock
对比表格在最后面。(其实写了之后我发现我需要复习JUC了)
1.早期的synchronized
JDK1.6之前属于重量级锁,依赖于操作系统的Mutex Lock,Java的线程映射到操作系统的原生线程,需要操作系统申请互斥量,操作系统对线程的切换,需要从用户态切换到内核态,比较耗时,效率低
2.对synchronized的优化
JDK1.6之后在JVM层面对synchronized底层做了很多的优化,包括偏向锁,轻量级锁,自旋锁,自适应自旋锁,锁消除,锁粗化等优化技术。
2.1 偏向锁
目的:在没有线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量的开销,提升性能。特点:
- 在没有锁竞争的情况下,会把整个锁消除
- 偏向于第一个获取到偏向锁的线程
- 如果在接下来的执行中偏向锁没有被其他线程获取,那么拥有该锁的线程就不需要同步
变化:在锁竞争激烈的场合,偏向锁失效。原因是,在此情况下,极有可能每次申请锁的线程不是同一个线程,所以此时不应该使用偏向锁,否则得不偿失。
2.2 轻量级锁
当偏向锁失效,JVM不会立即升级为重量级锁,而是试图使用轻量级锁的优化手段(JDK1.6之后加入的)。轻量级锁不是为了替代重量级锁,它的本意是是在没有线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量的开销,提升性能。
目的:和偏向锁一样
特点:
- 和偏向锁不同,轻量级锁使用CAS操作代替重量级锁。
- 使用轻量级锁,不需要申请互斥量。
- 轻量级锁的加锁和释放锁都是CAS操作。
变化:对于大多数锁来说,在整个同步周期都不存在竞争,这来自经验数据。如果没有竞争,轻量级锁使用CAS操作,避免了使用互斥锁的开销。如果存在竞争,除了互斥锁的开销,还会有额外的CAS操作,所以如果存在锁竞争,轻量级锁比重量级锁更慢。如果竞争激烈轻量级锁会迅速膨胀为重量级锁。关于轻量级锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。
2.3 自旋锁和自适应自旋锁
轻量级锁失效后,JVM避免线程真的在操作系统层面挂起,还会进行一项成为自旋锁的优化手段。在JDK1.6之前就有这项技术了,只是他是默认关闭的,可以通过参数--XX:+UseSpinning开启。JDK1.6之后默认开启。自旋不能完全替代阻塞,因为它还要占用处理器的时间。如果锁被占用的时间短,那么自旋锁的效果就好;否则,反之。自旋等待的时间必须固定,如果超过限定的次数,仍然没有获取到锁,就挂起线程。自旋默认10次,可以使用参数--XX:PreBlockSpin修改。
2.3.1 为什么会有自旋锁
互斥同步对性能最大的影响是阻塞的实现,因为线程的挂起和恢复都需要转入内核态去完成(用户态到内核态的转换将会耗费一定的时间)。而一般线程持有锁的时间并不会太长,如果仅仅为了这一点时间而挂起或恢复线程将会得不偿失。所以JVM团队就想:"能否让后面来的请求获取锁的线程等待一会儿而不被挂起?看看持有锁的线程是否很快就会释放锁"。目的:为了减少线程的挂起和恢复,减少带来的系统开销,引入自旋锁。
2.3.2 自旋的特点
执行忙循环
自旋次数固定(默认10次)
JDK1.6之前默认关闭,之后默认打开
效果的好坏依赖于锁被占用的时间的长短
3.Synchronized和ReEntrantLock对比
① 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
区别:
synchronized | Lock |
---|---|
JVM层面,底层:monitorenter /monitorexit |
API层面 |
不需要手动释放锁 | 需要手动释放 |
不能中断 | trylock 设超时时间,或lockInterruptibly 放入代码块设超时时间 |
只能是非公平锁 | 选择true 就变为公平锁 |
不能精准唤醒 | Lock 可以绑定多个Condition ,signal 某个flag 实现精准唤醒(我觉得这个不同点最重要) |
性能已不是选择标准!!
在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的, JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了。而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步。优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。
所以说,实际上是ReentrantReadWriteLock才因为可以“读读共存”都用myReadWriteLock.readLock().lock();
加锁并不互斥地读而高于synchronized
的性能,而单一的ReentrantLock
并没有比synchronized
性能高