把学习当成一种习惯
选择往往大于努力,越努力越幸运

前言

  基于并发编程基础: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并发编程实战【书籍】

目录