Java5.0增长了一种新的机制:ReentrantLock
,ReentrantLock并非一种替代内置加锁的方法,而是当内置加锁机制不适用时,做为一种可选择的高级功能。java
与内置的加锁机制不一样,Lcok提供了一种无条件的、可轮训的、定时的以及可中断的锁获取操做,全部的加锁和解锁的方法都是显式的。在Lock的实现中必须提供与内部锁相同的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能特性等方面能够有所不一样。算法
public interface Lock{ void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tyrLock(long timeout, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
互斥性
和内存可见性
:可重入的加锁语义
为何要建立一种与内置锁如此类似的新加锁机制?性能优化
Lock lock = new ReentrantLock(); ... lock.lock(); try{ // 更新对象状态 // 捕获异常,并在必要时恢复不变性条件 } finally { lock.unlock(); }
Lock的使用比内置锁复杂点,必须在finally中释放锁。不然若是在被保护的代码中抛出了异常,那么这个锁将永远不能被释放。
可定时的
与可轮训的
锁获取模式是由tryLock
方法实现的,与无条件的锁获取模式相比,它具备更完善的错误恢复机制。
在内置锁中,死锁是个至关严重的问题,恢复程序的惟一方法是从新启动,而防止死锁的惟一方法是在构造和编写程序的时候避免出现不一致的锁获取顺序。
而可定时的与可轮训的锁提供了另一种选择,避免死锁的发生。
若是不能得到全部须要的锁,那么可使用定时的或可轮训的锁获取方式,从而使你从新得到控制权,它会释放已经得到的锁,而后从新尝试获取全部锁。数据结构
public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException{ long fixedDelay = getFixedDelayComponentNanos(timeout, unit); long randMod = getRandomDelayModuluNanos(timeout, unit); long stopTime = System.nanoTime() + unit.toNanos(timeout); while(true){ if(fromAcct.lock.tryLock()){ try{ if(toAcct.lock.tryLock()){ try{ doSomething(); return true; } finally{ toAcct.lock.unlock(); } } } finally{ fromAcct.lock.unlock(); } } if(System.nanoTime() < stopTime){ return false; } NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); } }
使用tryLock来获取两个锁,若是不能同时得到,那么就回退并从新尝试。在休眠时间里包括固定部分和随机部分,从而下降发生活锁的可能性。若是在指定时间内不能得到全部须要的锁,那么将会返回一个false,从而使该操做平缓的失败。并发
在实现一个具备时间限制的操做时,定时锁一样很是有用。当在带有时间限制的操做中调用了一个阻塞方法,它能根据剩余时间来提供一个时限,若是操做不能在指定的时间给出结果,那么就会使程序提早结束。使用内置锁时,在开始请求锁后,这个操做就没法取消,所以内置锁很难实现带有时间限制的操做。dom
if(!lock.tryLock(nanosToLocks,NANOSECONDS)){ return false; } try{ doSomething(); } finally{ lock.unlock(); }
正如定时的锁获取操做能在带有时间限制的操做中使用独占锁,可中断的锁获取操做一样能在可取消的操做中使用加锁。lockInterruptibly
方法可以在得到锁的同时保持对中断的响应,而且因为它包含在Lock中,所以无需建立其余类型的不可中断阻塞机制。jvm
public boolean sendOnSharedLine(String message) throws InterruptedException{ lock.lockInterruptibly(); try{ return doSomething(message); } finally{ lock.unlock(); } private boolean doSomething(String message) throws InterruptedException{} }
可中断的锁获取操做的标准结构比普通的锁获取操做略微复杂一些,由于须要两个try块(若是在可中断锁操做中抛出InterruptedException,那么只须要常规的try-finally便可)。函数
对于同步原语来讲,竞争性能是可伸缩性的关键要素之一:若是有越多的资源被耗费在锁的管理和调度上,那么应用程序获得的资源就越少。
锁的实现越好,将须要越少的系统调用和上下文切换,而且在共享内存总线上的内存同步通讯量也越少,而一些耗时的操做将占用应用程序的计算资源。
jdk 6使用了改进后的算法来管理内置锁,与在ReentrantLock中使用的算法相似,该算法有效地提升了可伸缩性。内置锁的性能不会因为竞争而急剧降低,而且二者的可伸缩性也基本至关。高并发
在ReentrantLock的构造函数中提供了两种公平性选择:性能
非公平的锁(默认)
:容许插队,当一个线程请求非公平的锁时,若是在发出请求的同时,该锁状态变为可用,那么这个线程将跳过队列中全部的等待线程并得到这个锁(Semaphonre中一样能够选择采起公平或者非公平的获取顺序)。公平的锁
:线程按照它们发出请求的顺序来得到锁非公平的ReentrantLock并不提倡“插队”行为,但没法防止某个线程在合适的时候进行“插队”。(若是使用tryLock()方法,则得到一次插队机会)
当执行加锁操做时,公平性将因为在挂起线程和恢复线程时候存在开销而极大的下降性能。在实际状况下,统计上的公平性保证(确保被阻塞的线程能最终得到锁)一般已经够用了,而且实际上开销也能小不少。在大多数状况下,非公平锁的性能要高于公平锁的性能。
在激烈竞争的状况下,非公平锁的性能高于公平锁性能的一个缘由是:在恢复一个被挂起的线程于该线程真正开始运行之间存在着严重的延迟。
假设:线程A持有一个锁,而且线程B请求这个锁,因为这个锁已经被A持有了,所以B将被挂起,当A释放锁的时候,B将被唤醒,所以会再次尝试获取锁。于此同时,若是C也请求锁,那么C有可能会在B被彻底唤醒以前,得到、使用以及释放这个锁。这样是一个共赢局面:B得到锁的时刻并无推迟,C更早地得到了锁,而且吞吐量也得到了提升。
当持有锁的时间比较长,或者请求锁的平均时间间隔比较长,那么应该使用公平锁。在这种状况下,经过插队来提高吞吐量的状况可能不会出现。
与默认的ReentrantLock同样,内置加锁并不会提供肯定的公平性保证,可是在大多数状况下,在锁实现上实现统计的上的公平性保证已经足够了。java语言规范并无要求jvm以公平的方式来实现内置锁,jvm也没有这样作。
ReentrantLock实现了一种标准的互斥锁:每次最多只有一个线程能持有ReentrantLock。但对于维护数据的完整性来讲,互斥锁一般是一种过于强硬的加锁规则,因此也不太必要地限制了并发性。可是在许多状况下,数据结构上的操做都是读操做。此时,若是能放宽加锁需求,容许多个读操做同时访问数据结构,而且读取数据时候不会有其余线程修改数据,那么就不会有问题。
在如下状况下可使用读写锁:
ReadWriteLock接口
public interface ReadWriteLock{ Lock readLock(); Lock writeLock(); }
在读写锁的加锁策略中,容许多个读同时进行,可是每次只容许一个写操做。与Lock同样,ReadWriteLock能够采用多种不一样的实现方式,这些方式在性能、调度保证、获取优先性、公平性以及加锁语义等方面可能有所不一样。
读写锁是一种性能优化措施,在一些特定的状况下能够实现更好的并发性,在实际状况下,对于在多处理器上被频繁读取的数据结构,读写锁可以提升性能。而在其余状况下,读写锁的性能会比互斥锁更差一点,由于它们的复杂性更高。
因为ReadWriteLock使用Lock来实现锁的读写部分,所以若是分析结果代表读写锁没有提升性能,那么能够很容易的将读写锁换成独占锁。
在读取锁和写入锁之间的交互能够采用多种可选的实现方式:
释放优先
:当一个写入操做释放写入锁时,而且队列中同时存在读线程和写线程,那么应该优先选择读线程,写线程,仍是最早发出请求的线程?读线程插队
:若是锁是由读线程持有,可是写线程正在等待,那么新到达的读线程可否当即得到访问权,仍是应该在写线程后面等待?若是容许读线程插队到写线程以前,那么能提升并发性,但却可能形成写线程发生饥饿问题?重入性
:读取锁和写入锁是否能够重入降级
:若是一个线程持有写入锁,那么它可否在不释放该锁的状况下得到读取锁?这样可能会使得写入锁被降级为读取锁,同时不容许其余写线程修改被保护的资源。升级
:读取锁可否优先于其余正在等待的读线程和写线程而升级为一个写入锁?在大多数的读写锁实现中并不支持升级,由于若是没有显式的升级操做,很容易形成死锁(若是两个读线程都试图升级锁,那么两者都不会释放读取锁)。ReentrantReadWriteLock
为这两种锁都提供了可重入的加锁语义。与ReentrantLock相似,ReentrantReadWriteLock在构造的时候也能够选择一个非公平的锁(默认)或者一个公平锁。在公平的锁中,等待最长的线程将优先得到锁。若是这个锁由读线程持有,而另外一个线程请求写入锁,那么其余读线程都不能得到读取锁,知道写线程使用完成后并释放写入锁。在非公平的锁中,线程得到访问许可的顺序是不肯定的。写线程能够降级为读线程,可是不能从读线程升级到写线程。
与ReentrantLock相似的是,ReentrantReadWriteLock中的写入锁只能有惟一的全部者,而且只能由得到该锁的线程来释放。而读取锁经过记录那些线程已经获取了读取锁。
与内置锁相比,显式的Lock提供了一些拓展功能,在处理锁的不可用性方面有着更高的灵活性,而且对队列行有着更好的控制。但ReentrantLock不能彻底替代synchronized,只有在synchronized没法知足需求时,才应该使用它。读写锁容许多个读线程并发地访问被保护的对象,当访问以读取操做为主的数据结构时,它能提升程序的可伸缩性。