显式锁
11.1Lock和ReentrantLock
Lock接口源码:
//Lock提供了无条件的、可轮询的、定时的、可中断的锁获取操作。public interface Lock{void lock();void locaInterruptibly() throws InterruptedException;boolean tryLock();boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;void unlock();Condition newCondition();}
ReentrantLock实现了Lock接口,提供了与synchronized相同的互斥和内存可见性的保证。
获得ReentrantLock的锁与进入synchronized块有着相同的内存语义。
释放ReentrantLock的锁与退出synchronized块有着相同的内存语义。
ReentranLock提供了与synchronized一样的可重入加锁的语义。(可重入锁:可以重复获取相同的锁,但是需要释放,加锁次数一定要与释放次数保持一致,否则会死锁。synchronized会自动释放,ReentranLock需要手动释放)
ReentranLock支持Lock接口定义的所有获取锁的模式,与synchronized相比,ReentrantLock为处理不可用的锁提供了更多灵活性。
可重入演示:
public void test(){ReentrantLock lock = new ReentrantLock();new Thread(new Runnable(){public void run(){try{lock.lock();System.out.println("第一次获取锁");int index - 1;while(true){try{lock.lock();System.out.println("第"+ (++index) +"次获取锁");if(index == 10){break;}}finally{lock.unlock();}}}finally{lock.unlock();}}}).start();}
Lock接口使用的规范形式:
//锁必须在finally块中释放,如果锁守护的代码在try块之外抛出了异常,它将永远都不会被释放了。Lock lock = new ReentrantLock();...lock.lock();try{//更新对象的状态//捕获异常,必要时恢复到原来的不变约束}finally{lock.unlock();}
11.1.1可轮询的和可定时的锁请求
可定时的与可轮询的锁获取模式,是由tryLock方法实现,与无条件的锁获取相比,它具有更完善的错误恢复机制。
在内部锁中,死锁是致命的——唯一的恢复方法是重新启动程序,唯一的预防方法是在构建程序时不要出错。
可定时的与可轮询的锁提供了另一个选择:可以规避死锁的发生。
如果你不能获得所有需要的锁,那么使用tryLock使你能重新拿到控制权,它会释放你已经获得的这些锁,然后再重新尝试。休眠时间由一个特定的组件管理,并由一个随机组件减小活锁发生的可能性,如果一定时间内,没有能获得所有需要的锁,就会返回失败状态。
示例:
public boolean transferMoney(Account fromAcct,Account toAcct,DollarAmount amount,TimeUnit unit)throws InsufficientFundsException,InterruptedException{long fixedDelay = getFixedDelayComponentNanos(timeout, unit);long randMod = getRandomDelayModulusNanos(timeout, unit);long stopTime = System.nanoTime() + unit.toNanos(timeout);while(true){//使用tryLock避免锁顺序死锁//这里使用tryLock试图获得两个锁,如果不能同时获得两个,就回退,并重新尝试。if(fromAcct.lock.tryLock()){try{if(toAcct.lock.tryLock()){try{if(fromAcct.getBalance().compareTo(amount) < 0){throw new InsufficientFundsException();}else{fromAcct.debit(amount);toAcct.credit(amount);return true;}}finally{toAcct.lock.unlock();}}}finally{fromAcct.lock.unlock();}}if(System.nanoTime() < stopTime){return false;}NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);}}
示例:
//具有预定时间的锁public boolean trySendOnSharedLine(String message,long timeout,TimeUnit unit)throws InterruptedException{long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message);if(!lock.tryLock(nanosToLock, NANOSECONDS)){return false;}try{return sendOnSharedLine(message);}finally{lock.unlock();}}
11.1.2可中断的锁获取操作
正如定时锁的获得操作允许在限时活动内部使用独占锁。
可中断的锁获取操作允许在可取消的活动中使用。
当你正在响应中断的时候,lockInterruptibly方法能够使你获得锁,并且由于它是内置于Lock的,因此不必再创建其他种类不可中断的阻塞机制。
一个可中断的锁获取操作的规范形式:需要两个try块。
定时的tryLock同样响应中断,因此当你再需要获取定时和可中断的锁时可以使用tryLock这个方法。
示例:
//可中断的锁获取请求
public boolean sendOnSharedLine(String message) throws InterruptedException{
lock.lockInterruptibly();
try{//第一个try块,防止抛出InterruptedException
return cancellableSendOnSharedLine(message);
}finally{
lock.unlock();
}
}
public boolean trySendOnSharedLine(String message,
long timeout,
TimeUnit unit)
throws InterruptedException{
long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message);
if(!lock.tryLock(nanosToLock, NANOSECONDS)){
return false;
}
try{
return sendOnSharedLine(message);
}finally{
lock.unlock();
}
}
lock 与 lockInterruptibly比较区别在于:
- lock 优先考虑获取锁,待获取锁成功后,才响应中断。
- lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。
详细区别:
ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。
ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。
11.1.3非块结构的锁
在内部锁中,获取和释放这样成对的行为是块结构的。但是有时候需要更灵活的加锁规则。
例如:
在链表中,我们可以通过为每个链表节点应用相似的原则来减小锁的粒度,从而允许不同的线程独立地操作链表的不同部分。
如果要遍历或者修改链表,我们必须得得到响应的锁,并持有它直到获得了下一个节点的锁,之后,才能释放前一个节点的锁。这项技术的例子被称为**连锁式(锁联接)**。
11.2对性能的考量
java5.0中,ReentrantLock提供的竞争上的性能要远远优于内部锁。
java6.0中。改善了管理内部锁的算法,类似于ReentrantLock使用的算法,两者的性能非常接近。
竞争时的性能是可伸缩性的关键:如果有越多的资源花费在锁的管理和调度上,那么留给应用程序的就会越少。
性能和可伸缩性是基于平台的:比如CPU、处理器数量、高速缓存大小、JVM特性。所有这些因素都会随时间而改变。
性能是一个不断变化的目标,昨天的基准限时X比Y更快,但现在可能已经过时了。
11.3公平性
ReentrantLock构造函数提供了两种公平性的选择:(Semaphore同样提供了公平和非公平的获取顺序)
创建非公平锁(默认)
- 非公平锁允许“闯入”,当请求这样的锁时,如果锁的状态变为可用,线程的请求可以在等待线程的队列中向前跳跃,获得该锁。
创建公平锁
- 线程按顺序请求获得公平锁。
在公平锁中,如果锁已经被其他线程占有,新的线程会加入到等待队列,或者已经有一些线程在等待锁了。(可轮询的tryLock还是会“闯入”)
在非公平锁中,线程只有当锁正在被占用时才会等待。
为什么默认是非公平的锁?
因为:当发生加锁的时候,**公平锁会因为挂起和重新开始线程的代价带来巨大的性能开销**。而且,承诺一个阻塞的线程最终能够获得锁——通常已经够用了。除了有一些算法依赖公平的队列外,大多数情况下,**非公平锁性能的优势超过了公平的排队**。
为什么非公平锁更快?
因为:**挂起的线程重新开始,与它真正开始运行,两者之间会产生严重的延迟**。
例子:线程A持有一个锁,线程B请求该锁,此时锁正在使用,线程B会被挂起,当A释放锁后,B重新开始。与此同时,C请求该锁,那么C得到了很好的机会获得这个锁,使用它,并且甚至可能在B被唤醒前就已经释放了该锁。
B并没有比其他任何线程晚得到锁,C更早的得到了锁,吞吐量得到了改进。
与默认的ReentrantLock一样,内部锁没有提供确定的公平性保证。
11.4在synchronized和ReentrantLock之间进行选择
ReentrantLock与内部锁在加锁和内存语义上是相同的,在以下附加特性的语义上也是相同的,比如定时锁的等待、可中断锁的等待、公平性、以及实现非块结构的锁 。
在性能上,ReentrantLock的性能略微胜过synchronized。
但是内部锁相比于显式锁仍然具有很大的优势:
synchronized人们更熟悉,也更简洁。ReentrantLock是危险的,如果没有在finally块中调用unlock,你的代码将很可能看起来能够正常运行,但已经埋下了定时炸弹。
//只有当内部锁不能满足使用时,ReentrantLock才被作为更高级的工具。
//当你需要以下高级特性时,才应该使用:可定时的、可轮询的、可中断的锁获取操作、公平队列、或者非块结构的锁。否则,请使用synchronized。
ReentrantLock提供了一个管理和调试接口,锁可以使用这个接口进行注册,并通过其他管理和调试接口,从线程转储中得到ReentrantLock 的加锁信息。ReentrantLock的非块结构的特性仍然意味着获取锁不能依赖于特定的栈结构。
未来的性能改进可能更倾向于synchronized,因为syncrhonized是内置于JVM的,它能够进行优化,比如对线程限制的锁对象的锁省略,粗化锁来减小内部锁的同步性。
11.5读-写锁
ReentrantLock实现了标准的互斥锁:一次最多只有一个线程能够持有相同ReentrantLock。但是却过分地限制了并发性。(它甚至避免了“读/读”的情况)
读-写锁:一个资源能够被多个读者访问,或者被一个写者访问,两者不能同时进行。
ReadWriteLock接口源码:
//看起来是两个分离的锁,读取的锁和写入的锁只不过是同一的读-写锁对象的两个视角。
public interface ReadWriteLock{
Lock readLock();
Lock writedLock();
}
读写锁实现的加锁策略允许多个同时存在的读者,但是只允许一个写者。
与Lock一样,ReadWriteLock允许多种实现,造成了性能、调度保证、获取优先、公平性、以及加锁语义等方面的不尽相同。
读写锁的涉及是用来改进性能的,使得特定情况下能够有更好的并发性。在多处理器系统中,频繁的访问主要为读取数据结构的时候,读写锁能够改进性能;在其他情况下运行的情况比独占锁稍差一些,这归因于它更大的复杂性。
使用读写锁是,最好通过系统进行剖析来判断。
读取和写入锁之间的互动可以有很多种实现。ReadWriteLock的一些实现选择如下:
释放优先
- 当写者释放写入锁,并且读者和写者都排在队列中,应该选择哪个——读者,写者,还是先请求的那个?
读者闯入
- 如果锁由读者获得,但是有写者正在等待,那么新到达的写者应该被授予读取的权力么?还是应该等待?允许读者闯入到写者之前提高了并发性,但是却带来了写者饥饿的风险。
重进入
- 读取锁和写入锁允许重入吗?
降级
- 如果线程持有写入锁,它能够在不是放该锁的情况下获得读取锁吗?这可能会造成写者“降级”为一个读者,同时不允许其他写者修改这个被守护的资源。
升级
- 读取锁能够优先于其他的读者和写者升级为一个写入锁吗?大多数读写锁的实现不支持升级,因为在没有显式的升级操作的情况下,很容易造成死锁。(如果有两个读者试图升级到同一个写入锁,并都不释放资源)
ReentrantReadWriteLock为两个锁提供了可重进入的加锁语义。ReentrantReadWriteLock能够被构造成非公平(默认)或者是公平的。
在公平的锁中,选择权交给等待时间最长的线程:如果锁由读者获得,而一个线程请求写入锁,那么不再允许读者获得读写锁,直到写者被受理,并且已经释放了写入锁。
在非公平的锁中,线程允许访问的顺序是不定的。
写者可以降级为读者,读者不能升级为写者(会造成死锁)。
示例:
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 V put(K key, V value){
w.lock();
try{
return map.put(key, value);
}finally{
w.unlock();
}
}
public V get(Object key){
r.lock();
try{
return map.get(key);
}finally{
r.unlock();
}
}
}
总结
显式的Lock与内部锁相比提供了一些可扩展的特性:
处理不可用的锁时更好的灵活性,对队列行为更好的控制。
Reentrant Lock不能完全替代synchronized,只有当你需要synchronized没有提供的特性时才能使用。
读写锁允许多个读者并发访问被守护的对象,当访问多为读取数据结构的时候,它具有改进可伸缩性的潜力。
