《Java并发编程实战》 显式锁的使用
words: 2k views: time: 7min在java 5.0之前,协调共享对象的访问可以使用的机制有synchronized
和volatile
,java 5.0中新增了一种新的机制:ReentrantLock
。它的作用并不是用来替代内置锁,而是当内置锁不再适用时,作为一种可选择的高级功能。在java 6.0中,也使用了与ReentrantLock
类似的算法对内置锁本身进行了改进,有效地提高了内置锁的可伸缩性。
1. ReentrantLock
Lock
接口提供了一种无条件的、可轮询的、可限时的、以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的
1 | public interface Lock{ |
ReentrantLock
实现了Lock
接口,并提供了与synchronized
相同的互斥性和内存可见性。ReentrantLock
支持在Lock
接口中定义的所有获取锁模式,并且为处理锁的不可用性问题提供了更高的灵活性。但对这种锁的使用需要谨慎,必须在finally
块中释放锁,否则,如果在被保护的代码中抛出了异常,那么这个锁将永远无法释放。
1 | Lock lock = new ReentrantLock(); |
1.1. 轮询锁
可轮询或可定时的锁获取模式由tryLock
实现,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。在处理死锁的问题上,也提供了另一种选择
1 | public boolean transferMoney(Account fromAcct,Account toAcct, |
transferMoney
中使用tryLock
来获取两个锁,如果不能同时获得,那么就回退并重新尝试。并在休眠时间中添加随机部分,从而降低发生活锁的可能性。如果在指定时间内不能获得所有需要的锁,那么transferMoney
将返回一个失败的状态。
1.2. 定时锁
1 | Lock lock = new ReentrantLock(); |
tryLock
能够在带有时间限制的操作中实现独占的加锁行为。如示例中,试图在Lock
保护的共享通信线路上发送一条消息,如果已经获取锁的线程不能在指定时间内完成,即等待的线程不能在指定时间内获取锁,那等待的线程就会失败。
1.3. 可中断锁
1 | public boolean sendOnSharedLine(String message) throws InterruptedException{ |
lockInterruptibly
方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock
中,无须自己创建其他类型的不可中断阻塞机制,另外定时的tryLock
也同样能响应中断。
2. 公平性
ReentrantLock
的构造中提供了两种公平性的选择:创建一个非公平的锁(默认)或者一个公平的锁。
在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许插队:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。而在公平的锁中,如果有其他线程在队列中等待这个锁,那么新发出请求的线程将被放入队列中。在非公平的锁中,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中,因此,当执行加锁操作时,公平锁将由于在挂起线程和恢复线程时存在的开销而极大地降低性能。
非公平锁能提高性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程A
持有一个锁,如果线程B
请求这个锁,那么B
将被挂起。当A
释放锁时,B
将会被唤醒并再次尝试获取锁。与此同时,如果C
也请求这个锁,那么C
很可能在B
被完全唤醒之前完成获取、使用以及释放这个锁。这样的情况下,C
更早的获得了锁,而B
获得锁的时刻也没有推迟。可以想象,越在竞争激烈的情况下,越能体现非公平锁的优势。
3. 读写锁
ReentrantLock
实现了一种标准的互斥锁,互斥通常是一种强硬和保守的加锁策略。在许多情况下,数据结构上的操作都是读操作,如果能够放宽需求,允许多个读操作的线程同时访问数据结构,那么将提升程序的性能。
1 | public interface ReadWriteLock { |
ReadWriteLock
允许多个读操作同时进行,但每次只允许一个写操作。对于读取锁和写入锁之间的交互实现,有多种方式,这些方式在性能、调度保证、获取优先性、公平性以及加锁语义等方面会有所不同,具体的需要考虑以下问题:
释放优先:当一个写入操作释放时,并且队列中同时存在读线程和写线程,那么应该优先选择读线程、写线程、还是最先发出请求的线程;
读线程插队:如果锁由读线程持有,但是有写线程在等待,那么新到达的读线程能否立即获得访问权,还是应该在写线程后面等待?如果允许读线程插队,那么将提高并发性,但却可能造成写线程发生饥饿问题;
重入性 :读取锁和写入锁是否是可重入的;
降级:如果一个线程持有写入锁,那么它能否在不释放该锁的情况下获取读取锁?这可能会使得写入锁被降级为读取锁,同时不允许其他写线程修改被保护的资源;
升级 读取锁能否优先于其他正在等待的读线程或写线程而升级为一个写入锁?在大多数的读写锁实现中并不支持升级,因为如果没有显式的升级操作,那么很容易造成死锁,比如当两个读线程同时试图升级为写入锁,那么二者都不会释放读取锁。
ReentrantReadWriteLock
作为实现,默认提供非公平的锁并提供可重入的语义,它允许锁的降级操作但不支持升级
1 | public class ReadWriteMap<K, V> { |
通常,基于散列的ConcurrentHashMap
的性能已经很好了,如果需要对另一种Map实现提高并发访问性,比如LinkedHashMap
,则可以尝试如示例一样通过ReentrantReadWriteLock
来封装
参考:
- Copyright ©《java并发编程实战》