引:Java5.0以后提供了一种新的协调对共享对象的访问机制——ReentrantLock。它并不是用来替代内置锁的方法,而是当内置加锁不适用时,作为一种可选择的高级功能。
ReentrantLock
ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性,但是通常能提供更好的活跃性或性能。下面是Lock接口代码:1
2
3
4
5
6
7
8public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
为什么要创建一种与内置锁如此相似的新加锁价值?是由于内置锁具有以下的局限性:
- 无法中断一个正在等待获取锁的线程。
- 内置锁必须在获取锁的代码块中释放,这就简化了编码工作,并且与异常处理操作实现了很好的互动,但却无法实现非阻塞结构的加锁规则。
下面是Lock接口的标准使用形式:1
2
3
4
5
6
7
8
9Lock lock = new ReentrantLock();
...
lock.lock();
try {
//更新对象状态
//捕获异常,并在必要时恢复不变性条件
} finally {
lock.unlock(); //“定时炸弹”,一定要记得释放Lock
}
轮询锁与定时锁
可定时的与可轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。在内置锁中,死锁是一个严重的问题,恢复程序的唯一办法是重新启动程序,而防止死锁的唯一方法就是在构造程序时避免出现不一致的锁顺序。可定时的与可轮询的的锁提供了另一种选择,避免死锁的发生。通过重新获取及释放锁来避免死锁。
可中断的锁操作获取操作
可中断的锁操作获取操作是有lockInterruptibly或者tryLock方法实现的,如果在可中断的锁获取操作中抛出了InterruptedException,那么可以使用标准的try-finally加锁模式。
非块结构的加锁
我们通过Lock的使用结构可以知道,我们通过ReentrantLock可以灵活的实现锁的粒度。
公平性
大多数情况下,非公平锁的性能要高于公平锁的性能,原因是后者为了实现公平,会有更多的线程上下文切换成本。
当持有锁的时间较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。在这些情况下,允许“插队”带来的吞吐量提升(当锁处于可用的状态时,线程却还处于被唤醒的过程中)则可能不会出现。
在synchronized和ReentrantLock之间进行选择
在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列(默认非公平),以及非块结构的锁。否则,还是应该优先使用synchronized。
读-写锁
在读-写锁的加锁策略中,允许多个读操作同时进行,但每次只允许一个写操作。
ReentrantReadWriteLock为读锁和写锁都提供了可重入的加锁语义。ReentrantReadWriteLock在构造时可以选择是一个非公平的锁(默认)还是一个公平的锁。在公平的锁中,等待时间最长的线程将优先获得锁。在非公平的锁中,线程获得访问许可的顺序是不确定的。写线程可以降级为读线程,但是读线程不可以升级为写线程(因为多个读线程都不会放弃自己的读取锁而导致死锁)。
适用场景: 当锁的持有时间较长并且大部分操作都不会修改被守护的资源时,那么读-写锁能提高并发性。如果写操作也很频繁,那可能独占锁更合适一些,因为写操作太多,竞争会很激烈,再加上协调读写锁,性能反而不如独占锁了。
下面展示用读-写锁来包装Map:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30public class ReadWriteMap<K, V> {
private final Map<K, V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock r = lock.readLock();
private final Lock w = lock.writeLock();
public ReadWriteMap(Map<K, V> map) {
this.map = map;
}
public V put (K key, V value) {
w.lock();
try {
return map. put(key, value);
} finally {
w.unlock();
}
}
// 对remove(),putAll(),clear()等方法执行同样的操作
public V get(Object key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
// 对其他只读的Map方法执行相同的操作
}
总结
- 与内置锁相比,显示的Lock提供了一些扩展功能,在处理锁的不可用性方法有着更高的灵活性。但ReentrantLock不能完全替代synchronized,只有在synchronized无法满足需求时,才应该使用它。
- 读-写锁运行多个读线程并发地访问被保护的对象,当访问以读取操作为主的数据结构时,它能提高程序的可伸缩性。
参考
- 《Java并发编程实战》