前言
最近在看java并发编程这本书,已经看了点ReentrantLock的源码,以及之前有面试官问,公平锁和非公平锁有啥区别,我就只是从源码层面说了一下区别,但在性能上也有区别,今天就来说道说道。
公平与非公平
在ReentrantLock
的构造函数中提供了两种选择,默认创建非公平锁,也可以指定创建公平锁。
在公平锁上,线程将按照他们发出的请求顺序来获取锁,如果有另外一个线程持有这个锁或者有其他线程在队列中等待这个锁,那么新发出请求的线程就会被放到队列中。
在非公平锁上,允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,这个线程就会跳过队列中所有等待线程直接抢到这个锁。只有当锁被某个线程持有的时候,新发出请求的线程才会被放入队列中。
如何选择公平或非公平锁
为什么会有公平和非公平之分呢?我们都普遍认为一个公平的世界是一个稳定而温馨的世界,而不公平被认为不是好事,那么在锁的世界中如何呢?
当执行加锁操作时,公平性将由于挂起线程和恢复线程时存在的开销而大大降低性能。在实际的大多数情况中,非公平锁的性能要远高于公平锁的性能。
下图是Map的性能测试,并比较有公平的以及非公平的ReentrantLock
包装的HashMap的性能,从图中可以看出,公平性把性能降低了约2个数量级:
备注:ConcurrentHashMap
在线程数为4到8之间有一些波动,这个波动属于测量噪声,这种噪声在性能测试中常有,对整体结果不造成实质性影响可不做过度关注。
从上图可以看出来,在激烈竞争下,非公平锁的性能远高于公平锁的性能,其原因是:在恢复一个被挂起的线程 与线程真正开始运行之间存在着严重的延迟。
- 假设线程A持有一个锁,并且线程B请求这个锁。
- 由于这个锁已经被线程A持有了,线程B将被放入队列中挂起。
- 当A释放锁时,B将被唤醒,因此会再次尝试获取锁。
- 与此同时,正好有个C线程来请求这个锁,那么C线程很有可能在B被完全唤醒之前抢到,使用以及释放这个锁。
- 等C线程使用完释放后,B线程正好被唤醒,获取此锁并使用。
如此赢得了双赢的局面,线程B获取锁的时间并没有被延迟,C也在B唤醒的空挡中干完了自己的工作,因此最终的吞吐量也得以提高。
那么什么时候使用公平锁呢?
当持有的锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。在这种情况下,“插队”带来的吞吐量的提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)则可能不会出现。
ReentrantLock
与内置锁
说到锁就不得不提我们最熟悉的内置锁:Sychronized
,那么这两种锁又该如何选择呢?
ReentrantLock
相比内置锁还通了包括定时的锁等待,可中断的锁等待,公平性,以及实现非块结构的枷锁。其性能上似乎是优于内置锁,其中在Java6略有胜出,在Java5则远远胜出。既然如此,我们为什么不抛弃内置锁,直接选择ReentrantLock
呢?
原因在于:
ReentrantLock
的危险性比内置锁要高,如果忘记在finaly块中调用unlock方法,却虽然代码表面上可以正常运行,但却等于埋了一颗定时炸弹,还有可能伤及无辜。- 另外,
Sychronized
还有一个ReentrantLock
所没有的优点,就是在线程转储中能给出在哪些调用帧中获得了那些锁,并能够检测和识别发生死锁的线程。在Java6中还提供了给管理和调试接口,锁可以通过该接口进行注册,从而一些相关的加锁信息就能出现在线程转储中,给程序员带来一些帮助。而ReentrantLock
的非块状结构特性获取锁的操作不能与特定的栈帧关联起来,而内置锁却可以。 - 未来更可能会提升
Sychronized
而不是ReentrantLock
的性能。因为Sychronized
是JVM内置属性,它能执行一些优化,例如对线程封闭的锁对象的锁消除优化,Java6开始的锁升级的优化。
那么什么时候该选择ReentrantLock
呢?
在一些内置锁无法满足需求的情况下,ReentrantLock
可以作为一种高级工具。当需要一些高级功能的时候才应该使用ReentrantLock
,比如可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块状结构的锁。否则还是应该优先使用内置锁Sychronized
。
---------------你知道的越多,不知道的越多--------------