ReentrantLock(重进入锁)并不是作为内部锁(synchronized)机制的替代,而是当内部锁被证明受到局限时,提供可选择的高级特性.
1. Lock 和 ReentrantLock
Lock接口:
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
与内部加锁机制不同,Lock提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的方法都是显示的.
Lock的实现必须提供具有与内部加锁相同的内存可见性的语义.但是加锁的语义、调度算法、顺序保证,性能特性这些可以不同.
ReentrantLock实现了Lock接口,提供了与synchronized相同的互斥和内存可见性的保证.
获得ReentrantLock的锁与进入synchronized块有着相同的内存语义,释放ReentrantLock锁与退出synchronized块有相同的内存语义.
ReentrantLock提供了与synchronized一样的可重入加锁的语义.ReentrantLock支持Lock接口定义的所有获取锁的模式.
一句话synchronized能做的,ReentrantLock都能做,但是ReentrantLock为处理不可用的锁提供了更多灵活性(好吧,ReentrantLock写起来比较麻烦)
为什么要使用显示锁
内部锁在大部分情况下都能很好地工作,但是有一些功能上的局限--不能中断那些正在等待获取锁的线程,并且在请求锁失败的情况下,必须无限等待.
内部锁必须在获取他们的代码块中被释放:这很好地简化了代码,与异常处理机制能够良好的互动,但是在某些情况下,一个更灵活的加锁机制提供了更好的活跃度和性能.
public class LockTest { Lock lock = new ReentrantLock(); public void testLock(){ lock.Lock(); try { // 需要加锁的代码.. }finally { lock.unlock(); } } }
这个模式在某种程度上比使用内部锁更加复杂:锁必须在finally块中释放.
另一方面,如果锁守护的代码在try块之外抛出了异常,它将永远都不会被释放了;
如果对象能够被置于不一致的状态,可能需要额外的try-catch,或try-finally块.
显示的lock的缺点
使用lock之后必须unlock释放锁,这也是ReentrantLock不能完全替代synchronized的原因.
它更加危险,因为当程序的控制权离开了守护的块时,不会自动清除锁.
1.1 可轮询和可定时的锁请求
可定时的与可轮询的锁获取模式,是由tryLock方法实现,与无条件的锁获取相比,它具有更完善的错误恢复机制.
使用内部锁,发生死锁时唯一的恢复方法是重启程序,唯一的预防方法是在构建程序时不要出错,所以不可能允许不一致的锁顺序.
可定时的与可轮询的锁提供了另一个选择,可以规避死锁的发生.
使用方式:
public class LockSample { //创建一个锁的实例 Lock lock = new ReentrantLock(); public void methodA(){ lock.lock(); try { System.out.println("执行了方法A"); Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void methodB(){ lock.lock(); try { System.out.println("执行了方法B"); Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String [] args){ LockSample lockSample = new LockSample(); lockSample.methodA(); //methodB()方法必须在锁可用的时候才会执行 lockSample.methodB(); } }
使用tryLock能解决第九篇博客死锁,提到过的动态的顺序死锁问题.
public class LockTest { public static void main(String [] args){ LockTest lockTest = new LockTest(); Account fromAccount = new Account(); Account toAccount = new Account(); Account account = new Account(); //开启一个新线程,获取两个用户的锁,这个方法是假设,对象的锁已经被获得用的. new Thread(){ @Override public void run(){ //这两个方法的内部实现就是Thread.sleep()将代码阻塞住. fromAccount.credit(account); toAccount.dedit(account); } }.start(); lockTest.transferMoney(fromAccount,toAccount,account); } public void transferMoney(Account fromAccount,Account toAccount,Account account){ while(true){ // lock.tryLock()返回一个布尔值,告诉你当前的锁是否可用,如果可用往下走 if(fromAccount.lock.tryLock()){ try { if (toAccount.lock.tryLock()){ try { //走到这里,证明两个锁都可用,可以进行转账操作. fromAccount.credit(account); toAccount.dedit(account); }finally { toAccount.lock.unlock(); } } }finally { fromAccount.lock.unlock(); } } } } }
Account的内部实现:
public class Account { public Lock lock = new ReentrantLock(); public void credit(Account account) { lock.lock(); try { try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } finally { lock.unlock(); } } public void dedit(Account account) { lock.lock(); try { try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } finally { lock.unlock(); } }
定时锁可以在时间预算内设定相应的超时,如果活动子啊期待的时间内没能获得结果,这个机制是程序能够提前返回.
而使用内部锁一旦开始请求,锁就不能停止了,所以内部锁为实现具有时限的活动带来了风险.
.tryLock方法还有一个重载版本,可以设定等待的时间:
lock.tryLock(4, TimeUnit.SECONDS)
1.2 可中断的锁获取操作
lock.lockInterruptibly上的锁,是可以响应中断的:
public class LockSample { //创建一个锁的实例 Lock lock = new ReentrantLock(); public void testInterruptibly(){ try { lock.lockInterruptibly(); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void test(){ System.out.println("lock.tryLock() = " + lock.tryLock()); try { System.out.println("lock.tryLock(4,TimeUnit.SECONDS) = " + lock.tryLock(4, TimeUnit.SECONDS));; } catch (InterruptedException e) { e.printStackTrace(); } } public void methodA(){ lock.lock(); try { System.out.println("执行了方法A"); Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void methodB(){ lock.lock(); try { System.out.println("执行了方法B"); Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String [] args){ Long startTime = System.nanoTime(); LockSample lockSample = new LockSample(); Thread thread = Thread.currentThread(); new Thread(){ @Override public void run(){ try { //休眠两秒,执行中断 Thread.sleep(2000); thread.interrupt(); } catch (InterruptedException e) { e.printStackTrace(); } } }.start(); //这里本来是休眠5秒的,因为上面直接中断了,可以看下面的endtime是两秒,证明了可以被中断 lockSample.testInterruptibly(); Long endTime = startTime - System.nanoTime(); System.out.println("endTime = " + endTime); } }
2. 对性能的考量
ReentrantLock提供的竞争上的性能要远远优于内部锁.
对于同步原语而言,竞态时的性能是可伸缩性的关键:若果有越多的资源花费在锁的管理和调度上,那程序执行的时间就越少.
在Java5.0中,ReentrantLock相比于synchronized能给吞吐量带来相当不错的提升,但是在Java6中,这两者非常接近.
也就是说之前选择显示锁,还有性能方面的考量,但是现在显示锁和synchronized已经差不多了.
3. 公平性
ReentrantLock构造函数提供了两种公平性的选择:
创建非公平锁(默认)
公平锁
公平锁:如果锁已经被其他线程占有,新的请求线程会加入到等待队列,或者已经有一些线程在等待锁了;
非公平锁: 非公平锁允许闯入,当请求这样的锁时,如果锁的状态变为可用,线程的请求可以在等待线程的队列中向前跳跃,获得该锁.(Semaphore同样提供了公平和非公平的获取顺序).在非公平的锁中,线程只有当锁正在被占用时才会等待.
为什么要使用不公平锁
当发生加锁的时候,公平会因为挂起和重新开始线程的代价带来巨大的性能开销.
在多数情况下,非公平锁的优势超过了公平的排队.
在竞争激烈的情况下,闯入锁比公平锁性能好的原因之一是:挂起的线程重新开始,与它真正开始运行,两者之间会产生严重的延迟.
比较公平锁和非公平锁,使用的例子:
假设线程A持有一个锁,线程B请求该锁.因为此时锁正在使用中,线程B被挂起,当A释放锁后,B重新开始.与此同时,如果C请求锁,那么C得到了很好的机会获得这个锁,使用它,并且甚至可能在B被唤醒前就已经释放该锁了.
在这样的情况下,各方面都获得了成功:B并没有比其他任何线程晚得到锁,C更早的得到了锁,吞吐量得到了改进.
如果持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么使用公平锁是比较好的.
4. 在synchronized和ReentrantLock之间进行选择
在内部锁不能够满足使用时,ReentrantLock才被作为更高级的工具,当你需要以下高级特性时,才应该使用: 可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁,否则,请使用synchronized.
5. 读-写锁
读-写锁:一个资源能够被多个读者访问,或者被一个写者访问,两者不能同时进行.
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }
ReadWriteLock暴露了两个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 V put(K key,V value){ w.lock(); try { return map.put(key,value); }finally { w.unlock(); } } //remove(),putAll(),clear()使用w.lock public V get(Object key){ r.lock(); try { return map.get(key); }finally { r.unlock(); } } //其他的只读map使用r.lock}
总结
显示的Lock与内部锁相比提供了一些扩展的特性,包括处理不可用的锁时更好的灵活性,以及对队列行为更好的控制,但是ReentrantLock不能完全替代synchronized;只有当你需要synchronized没能提供的特性时才应该使用.
读-写锁允许多个读者并发访问被守护的对象,当访问多为读取数据结构的时候,它具有改进可伸缩性的能力.