一.Lock和ReentrantLock
与内部加锁机制不同,lock提供了无条件的,可轮询的,定时的,可中断的锁获取操作,所有的加锁和解锁的方法都是显示的。
lock的锁更加复杂:锁必须在finally中释放,另一个方面,如果锁守护的代码在try块之外抛出了异常,它永远不会被释放;如果对象能够被置于不一致的状态,可能需要额外的try-catch,或try-finally。
1.可轮询的和可定时的锁请求
可定时的和可轮询的锁获取模式,是由tryLock方法实现。与无条件的锁获取相比它它具有更完善的错误恢复机制。内部锁中唯一的恢复方法是重行启动程序,唯一的构建方法是构建程序时不要出错。可轮询和可定时的锁提供了另一个锁选择:可以避免死锁的发生。
如果你不能获取所有需要的锁,那么使用可定时的和可轮询的获取方式使你能够重新拿到控制权,它会释放你已经获得的这些锁,然后再重新尝试。(至少会记录这个失败,或采取其他措施)。
对于那些具有时间限制的活动,当它们调用了阻塞方法,定时锁能够在时间预算内设定响应的超时。如果活动在时间内没能获得结果,这个机制使程序能够提前返回。使用内部锁一旦开始请求,锁就不能停止了。
2.可中断的锁获取操作
可中断的锁获取操作允许在可取消的活动中使用。当你正在响应中断的是,lockInterruptibly方法使你能获得锁,并且由于它是内置Lock的,因此你不必在创建其他种类不可中断的阻塞机制。
使用tryLock避免顺序死锁
public boolean transferMoney(final Account fromAccount, final Account toAccount, final DollarAmount amount,long timeout, TimeUnit unit) throws InterruptedException {
long fixedDelay = 123;
long randMod = 456;
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while (true) {
if (fromAccount.getLock().tryLock()) { // 如果不能获得锁就返回重试
try {
if (toAccount.getLock().tryLock()) {
try {
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new RuntimeException();
} else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
} finally {
toAccount.getLock().unlock();
}
}
} finally {
fromAccount.getLock().unlock();
}
}
if (System.nanoTime() > stopTime) { // 重试了指定次数仍然无法获得锁则返回失败
return false;
}
Thread.sleep(fixedDelay + new Random().nextLong() % randMod);
}
}
使用预定时间的锁
public boolean trySendOneSharedLine(String message, long timeout, TimeUnit unit) throws InterruptedException {
long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message);
if (lock.tryLock(nanosToLock, TimeUnit.NANOSECONDS)) {
return false;
}
try {
return sendOnSharedLined(message);
}finally {
lock.unlock();
}
}
可中断的锁获取请求
public boolean sendOnSharedLine(String message)throws InterruptedException{
lock.lockInterruptibly();
try{
return cancellableSendOnSharedLine(message);
}finally{
lock.unlock();
}
}
private boolean cancellableSendOnSharedLine(String message)throws InterruptedException{
...
}
3.非块结构的锁
在内部锁中,获取和释放这样的行为是块结构的-总是在其获得的相同的基本程序块中释放,而不考虑控制权是如何退出阻塞块的。
在链表中,我们可以通过每个链表节点应用分离锁来减小锁的粒度,给定节点的锁守护链接的指针,所以要遍历或修改链表,我们必须得到这个锁,并持有它直到我们获得了下一个锁;这之后我盟才能释放前一个锁。这项技术被称作连式锁,或者锁联接。
二.对性能的考量
当ReentrantLock被加入到java5.0时它提供的竞争上的性能要远远优于内部锁。
1.公平性
ReentrantLock构造函数提供了俩种公平性选择:创建非公平锁(默认)或者公平锁。
线程按顺序请求获得公平锁,非公平锁允许“闯入”:当请求到这样的锁时,如果锁的状态变为可用,线程的请求可以在等待线程的队列中向前跳跃获得该锁。(Semaphore同样提供了公平的和非公平的获取顺序)。
4.在synchronized和ReentrantLock之间进行选择
内部锁相比于显示锁有很大的优势。它更加简洁。ReentrantLock绝对是最危险的同步工具。内部锁与ReentrantLock相比,还有另外一个优点:线程转储能够显示哪些个调用框架获得了哪些锁,并能够识别发生了死锁的那些线程。
java中提供了管理和调试接口,可以使用这个接口进行注册,并通过其他管理和调试接口,从线程转储中得到ReentrantLock的加锁信息。
未来的性能改进可能更倾向于synchronized;因为它是内置于JVM的,它能够进行优化。
在内部锁不能足够使用时,ReentrantLock才被作为更高级的工具使用。
5.读写锁
ReentrantLock实现了标准互斥锁;一次最多只有一个线程能够持有相同ReentrantLock。但是互斥锁过分的限制了并发性。
读写锁:一个资源能够被多个读者访问,或者被一个写者访问,两者不能同时进行。与Lock一样ReadWriteLock允许多种实现,造成了性能、调度保证、获取优先、公平性、以及加锁语义方面不尽相同。
在频繁读的情况下读写锁能够改进性能,在其他情况下允许的情况下比独占锁要稍差一些,这归根于它更大的复杂性。
读取和写入的互动有多种实现。ReadWriteLock的一些实现选择如下:
- 释放优先。当写者释放写入锁,并且读者和写者都排在队列中,应该选择哪一个,读者写者,还是先请求的那个?
- 读者闯入。如果锁由读者获得,但有写者正在等待,那么写到达的写者应该被授予读取的权利么?还是应该等待?允许读者闯入到写者之前提高了并发性,但是却带来了写者饥饿的问题。
- 重进入。读取锁和写入锁允许重进入吗?
- 降级。如果线程持有写入的锁,它能够在不释放锁的情况下获取读取的锁吗?这可能造成写者“降级”为一个读取锁,同时不允许其它写着修改这个被守护的资源
- 升级。读取锁能够优先于其他的读者和写者升级为一个写入锁么?大多数读写锁时间并不支持升级,因为在没有显示的升级的操作的情况下,很容易造成死锁。(如果俩个读者同时试图升级到同一个写入锁,并不释放读取锁)。
ReentrantReadWriteLock为俩个锁提供了可重进入的加锁语义。它能够被构造为非公平(默认)或者是公平的。在公平的锁中,选择权交给等待时间最长的线程;如果锁由读者获得,而一个线程请求写入锁,那么不再允许读者获得读取锁。直到写者被受理,并且已经释放了写入锁,在非公平锁中线程允许访问顺序是不定的。由写者降级为读者是允许的;从读者升级为写者是不允许的。
用读写锁包装的map
public 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 void 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方法也完全类似
}