
前言
基于并发编程基础:Java对象头及锁升级这篇文章,我们可以知道JDK1.6版本引入偏向锁 --> 低频的自旋尝试获取锁(即轻量级锁) --> 最后升级为重量级锁 的竞争锁方式,对Synchronized做出了极大的优化,性能也得到了很大的提升.
基于并发编程基础:管程及Synchronized这篇文章,我们可以知道Synchronized升级为重量级锁后的底层实现原理.
基于并发编程基础:ReentrantLock之AQS独占式源码分析这篇文章,我们可以知道ReentrantLock的底层实现原理.
通过这三篇文章,对Synchronized、ReentrantLock的底层原理实现有了一个质的认识,本文将对于这两个锁作出比较及选择.
共同点
- 解决了并发的两大核心问题 : 互斥和同步
- Synchronized : 解决了互斥问题,并且结合wait()、notify()、notifyAll()等函数解决了同步问题
- ReentrantLock : 解决了互斥问题,并且结合Condition及await()、signal()、signalAll()等函数解决了同步问题
- 独占式获取锁 : 即只能一条线程能持有锁状态
- 竞争激烈的情况下,都会挂起阻塞进入同步队列
- 两者都是可重入锁
不同点
- Synchronized是内置锁,在JVM层面实现,而ReentrantLock是显示锁,代码层面实现.
- 内置锁竞争锁失败后,加入同步队列之前会一直在尝试竞争锁(自旋入队期间,如果入队失败还是会尝试获取锁);而显示锁在第一次竞争锁失败后,就没有继续去再次竞争锁了(除非prev结点的head结点),而是入队.
- 内置锁是非公平锁,而显示锁默认是非公平锁,并且可以通过构造器设置为公平锁.
- 内置锁至多只能维护一条条件队列,而显示锁可以维护多条条件队列(多个Condition对象).
- 内置锁是自动释放锁,而显示锁需要通过调用unlock()释放锁.
- 内置锁竞争锁期间不响应中断,而显示锁竞争锁期间可响应中断.
- 内置锁不能设置超时中断竞争锁,而显示锁可以通过超时中断竞争锁.
- 内置锁在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而显示锁在发生异常时,如果没有主动通过 unlock() 去释放锁,则很可能造成死锁现象,因此使用显示锁时需要在 finally 块中释放锁.
- 显示锁可以知道有没有成功获取锁,而内置锁却无法办到.
小结
显示锁相对于内置锁来说比较灵活,但是风险性比内置锁高,因此使用显示锁时如果在 finally 块中没有释放锁,则可能会出现死锁现象,死锁是一种很严重的并发问题.
两者的分析及建议
分析
- 显示锁在加锁和内存上提供的语义与内置锁相同,相比于内置锁,显示锁比较灵活,可支持定时的锁等待、可中断的锁等待、可轮询、公平性.
- 对于内置锁在JDK1.6版本做出优化后,其实性能方面,两者相差不了多少了,显示锁略有胜出;而JDK1.5版本则是远远胜出.
- 内置锁的存在大幅度简化了Java多线程的开发,相比之显示锁的使用就繁琐的多了,你加完锁之后还得考虑到各种情况下的锁释放,稍不留神就一个bug埋下了.
- JDK1.5版本中,内置锁相比于显示锁还存在于一个优点 : 在线程存储中能够检测和识别发生死锁的线程,而JVM不能知道哪条线程持有显示锁,因此调试使用显示锁的线程的问题上,将起不到帮助作用.在JDK1.6版本解决了这个问题,即提供了一个管理和调试接口,锁可以通过该接口和调试接口来访问.
- 虚拟机在未来的改进中更偏向于原生的synchronized而不是ReentrantLock的性能,因为内置锁是JVM的内置属性,它能执行一些优化.
建议
综合上面知识点的分析,除非你的系统将来需要在JDK1.5版本上部署,或者需要ReentrantLock包含的可伸缩性(或者灵活性),否则性能方面来说还是选择synchronized而不是ReentrantLock.
结束语
- 原创不易
- 希望看完这篇文章的你有所收获!
相关参考资料
- JDK1.8
- Java并发编程实战【书籍】