第十三章 显式锁
一 Lock 与 ReentrantLock
与内置加锁机制不同的是,LOck提供了一种无条件的,可轮训的,定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。
public interface Lock{
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout,TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
和内置锁的区别:
- ReentrantLock实现了Lock接口,提供了与synchronized同样的互斥性和可见性,也同样提供了可重入性。
- 内置锁存在一些功能限制:无法中断一个正在等待获取锁的线程,无法获取一个锁时无限得等待下去。ReentrantLock更加灵活,能提供更好的活跃性和性能
- 内置锁的释放时自动的,而ReentrantLock的释放必须在finally手动释放,因此除了需要显式锁的定时等功能时,就性能方面来说,应该选择内置锁。
如果不能获得所有需要的锁,那么可以使用可定时的或可轮询的锁获取方式,从而重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有锁。
tryLock将尝试加锁,并返回加锁结果,因此可以当加锁失败时执行其他操作
lockInterruptibly可抛出一个InterruptedException,可在捕获异常之后进行操作
二 公平性
在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认),或者创建一个公平锁。在公平锁上,线程将按照他们请求的顺序来获得锁,但在非公平锁上,则允许插队:当一个线程请求非公平锁时,如果在发出请求的同时该锁变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。
一般非公平锁的性能大于公平锁的性能,因为在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁,由于A已经持有这个锁了,所以B会被挂起。当A释放锁时,B开始被唤醒。如果B唤醒的时间比较长,与此同时,线程C也请求这个锁,并且在B完全唤醒之前,C获得使用并且释放了这个锁,这就达到了双赢的局面。
三 读写锁
一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时访问。
读—写锁有一下需要考虑的点:
当一个写线程释放锁时,队列中同时存在一个写线程和读线程,哪个应该先被调度?
在公平锁中,等待时间最长的线程优先获得锁;在非公平锁中,线程调度的顺序是不确定的。如果锁由读线程持有,当前写线程正在等待,如果新到达的读线程能够插队立即获得锁的访问权?还是应该在写线程后面等待?
在公平锁中,如果持有锁的是读线程,而写线程正在请求写入锁,那么其他读线程都不能获取读取锁,直到写线程使用完并释放了写入锁。读取锁和写入锁是否是可重入的?
读取所和写入锁都是可重入的如果一个线程持有写入锁,那么它能够在不释放写入锁的同时获取读取锁?即写入锁降级为读取锁
写入锁降级为读取锁是允许的- 读取锁能够优先于其它正在等待的写入锁和读取锁而升级为一个写入锁?
读取锁升级为写入锁是不允许的,因为这可能造成死锁(如果两个读取锁都升级为写入锁,那么两者都不会释放读取锁)
一个用读—写锁来包装map
public class LockMap<K, V>{
private final Map<K, V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock rLock = lock.readLock();
private final Lock wLock = lock.writeLock();
public Test(Map<K, V> map){
this.map = map;
}
public void put(K key, V value){
wLock.lock();
try {
map.put(key, value);
} finally{
wLock.unlock();//一定要记得释放
}
}
public V get(K key){
rLock.lock();
try {
return map.get(key);
} finally{
rLock.unlock();//一定要记得释放
}
}
}
读的时候上读锁,写的时候上写锁