并发编程学习笔记之显示锁(十一)

发布日期:2019-01-17

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(fromAccounttoAccountaccount) } public void transferMoney(Account fromAccountAccount toAccountAccount 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(4TimeUnit.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<KV> { private final Map<KV> 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 keyV value){ w.lock() try { return map.put(keyvalue) }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没能提供的特性时才应该使用.

读-写锁允许多个读者并发访问被守护的对象当访问多为读取数据结构的时候它具有改进可伸缩性的能力.

1 0 9)