Java并发之Condition与Lock

        java.util.concurrent.locks包为锁和等待条件提供一个框架的接口和类,它不一样于内置同步和监视器。该框架容许更灵活地使用锁和条件,但以更难用的语法为代价。  javascript

        Lock 接口支持那些语义不一样(重入、公平等)的锁规则,能够在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。  java

        ReadWriteLock 接口以相似方式定义了一些读取者能够共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,由于它适用于大部分的标准用法上下文。但程序员能够建立本身的、适用于非标准要求的实现。  程序员

        Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器相似,但提供了更强大的功能。须要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了不兼容性问题,Condition 方法的名称与对应的 Object 版本中的不一样。  算法

        如下是locks包的相关类图: 数据库

 


 

        在以前咱们同步一段代码或者对象时都是使用 synchronized关键字,使用的是Java语言的内置特性,然而 synchronized的特性也致使了不少场景下出现问题,好比: 编程

        在一段同步资源上,首先线程A得到了该资源的锁,并开始执行,此时其余想要操做此资源的线程就必须等待。若是线程A由于某些缘由而处于长时间操做的状态,好比等待网络,反复重试等等。那么其余线程就没有办法及时的处理它们的任务,只能无限制的等待下去。若是线程A的锁在持有一段时间后可自动被释放,那么其余线程不就可使用该资源了吗?再有就是相似于数据库中的共享锁与排它锁,是否也能够应用到应用程序中?因此引入Lock机制就能够很好的解决这些问题。 缓存

  Lock提供了比 synchronized更多的功能。可是要注意如下几点: 网络

   Lock不是Java语言内置的,synchronized是Java语言的关键字,所以是内置特性。Lock是一个类,经过这个类能够实现同步访问; 并发

   Lock和synchronized有一点很是大的不一样,采用 synchronized不须要用户去手动释放锁,当synchronized方法或者 synchronized代码块执行完以后,系统会自动让线程释放对锁的占用;而 Lock则必需要用户去手动释放锁,若是没有主动释放锁,就有可能致使出现死锁现象。 app

 

        1、Condition

        Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成大相径庭的对象,以便经过将这些对象与任意 Lock 实现组合使用,为每一个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。 

        Condition(也称为条件队列 或条件变量)为线程提供了一种手段,在某个状态条件下直到接到另外一个线程的通知,一直处于挂起状态(即“等待”)。由于访问此共享状态信息发生在不一样的线程中,因此它必须受到保护,所以要将某种形式的锁与 Condition相关联。

        Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例得到 Condition 实例,可使用其 newCondition() 方法,如下是API中的实例:

Java代码    收藏代码
  1. import java.util.concurrent.locks.Condition;  
  2. import java.util.concurrent.locks.Lock;  
  3. import java.util.concurrent.locks.ReentrantLock;  
  4.   
  5. public class BoundedBuffer {  
  6.     final Lock lock = new ReentrantLock();// 锁对象  
  7.     final Condition notFull = lock.newCondition();// 写线程条件  
  8.     final Condition notEmpty = lock.newCondition();// 读线程条件  
  9.   
  10.     final Integer[] items = new Integer[10];// 缓存队列  
  11.     int putptr/* 写索引 */, takeptr/* 读索引 */, count/* 队列中存在的数据个数 */;  
  12.   
  13.     public void put(Integer x) throws InterruptedException {  
  14.         lock.lock();  
  15.         try {  
  16.             while (count == items.length)  
  17.                 // 若是队列满了  
  18.                 notFull.await();// 阻塞写线程  
  19.             items[putptr] = x;// 赋值  
  20.             System.out.println("写入:" + x);  
  21.             if (++putptr == items.length)  
  22.                 putptr = 0;// 若是写索引写到队列的最后一个位置了,那么置为0  
  23.             ++count;// 个数++  
  24.             notEmpty.signal();// 唤醒读线程  
  25.         } finally {  
  26.             lock.unlock();  
  27.         }  
  28.     }  
  29.   
  30.     public Integer take() throws InterruptedException {  
  31.         lock.lock();  
  32.         try {  
  33.             while (count == 0)  
  34.                 // 若是队列为空  
  35.                 notEmpty.await();// 阻塞读线程  
  36.             Integer x = items[takeptr];// 取值  
  37.             System.out.println("读取:" + x);  
  38.             if (++takeptr == items.length)  
  39.                 takeptr = 0;// 若是读索引读到队列的最后一个位置了,那么置为0  
  40.             --count;// 个数--  
  41.             notFull.signal();// 唤醒写线程  
  42.             return x;  
  43.         } finally {  
  44.             lock.unlock();  
  45.         }  
  46.     }  
  47.   
  48.     public static void main(String[] args) {  
  49.         final BoundedBuffer b = new BoundedBuffer();  
  50.   
  51.         new Thread(new Runnable() {  
  52.             public void run() {  
  53.                 int i = 0;  
  54.                 while (true) {  
  55.                     try {  
  56.                         b.put(i++);  
  57.                     } catch (InterruptedException e) {  
  58.                         e.printStackTrace();  
  59.                     }  
  60.                 }  
  61.             }  
  62.         }).start();  
  63.         new Thread(new Runnable() {  
  64.             public void run() {  
  65.                 while (true) {  
  66.                     try {  
  67.                         b.take();  
  68.                     } catch (InterruptedException e) {  
  69.                         e.printStackTrace();  
  70.                     }  
  71.                 }  
  72.             }  
  73.         }).start();  
  74.     }  
  75. }  
  76. //结果:  
  77. 写入:0  
  78. 写入:1  
  79. 写入:2  
  80. 写入:3  
  81. 写入:4  
  82. 写入:5  
  83. 写入:6  
  84. 写入:7  
  85. 写入:8  
  86. 写入:9  
  87. 读取:0  
  88. 读取:1  
  89. 读取:2  
  90. 读取:3  
  91. 读取:4  
  92. 读取:5  
  93. 读取:6  
  94. 读取:7  
  95. 读取:8  
  96. 读取:9  
  97. ...  
  98. ...  
  99. ...  

        做为一个示例,假定有一个绑定的缓冲区,它支持 put 和 take 方法。若是试图在空的缓冲区上执行 take 操做,则在某一个项变得可用以前(写操做前),线程将一直阻塞;若是试图在满的缓冲区上执行 put 操做,则在有空间变得可用以前(读操做前),线程将一直阻塞。从程序的运行结果就能够看出,缓冲区未满以前写操做持续进行,此时读操做会被阻塞没法读取数据。当缓冲区写满后写线程唤醒读线程,让其开始工做,此时读线程将缓冲区中的数据不断取出,直至缓冲区中全部数据都被取出,以后读线程进行等待并通知写操做开始工做。

        在Condition中,用 await()代替 wait(),用 signal()代替 notify(),用 signalAll()代替 notifyAll(),传统线程的通讯方式,Condition均可以实现,Condition的强大之处在于它能够为多个线程间创建不一样的 Condition。

        Condition 实现能够提供不一样于 Object 监视器方法的行为和语义,好比受保证的通知排序,或者在执行通知时不须要保持一个锁。若是某个实现提供了这样特殊的语义,则该实现必须记录这些语义。 

        注意,Condition 实例只是一些普通的对象,它们自身能够用做 synchronized 语句中的目标,而且能够调用本身的 wait 和 notification 监视器方法。获取 Condition 实例的监视器锁或者使用其监视器方法,与获取和该 Condition 相关的 Lock 或使用其 waiting 和 signalling 方法没有什么特定的关系。为了不混淆,建议除了在其自身的实现中以外,切勿以这种方式使用 Condition 实例。 

        除非另行说明,不然为任何参数传递 null 值将致使抛出 NullPointerException。 

        Condition方法很少,以前已经提到了await和 signal两个方法,如下是 Condition的所有方法:

Java代码    收藏代码
  1. // 形成当前线程在接到信号或被中断以前一直处于等待状态。  
  2. void await()  
  3.   
  4. // 形成当前线程在接到信号、被中断或到达指定等待时间以前一直处于等待状态。  
  5. boolean await(long time, TimeUnit unit)  
  6.   
  7. // 形成当前线程在接到信号、被中断或到达指定等待时间以前一直处于等待状态。  
  8. long awaitNanos(long nanosTimeout)  
  9.   
  10. // 形成当前线程在接到信号以前一直处于等待状态。  
  11. void awaitUninterruptibly()  
  12.   
  13. // 形成当前线程在接到信号、被中断或到达指定最后期限以前一直处于等待状态。  
  14. boolean awaitUntil(Date deadline)  
  15.   
  16. // 唤醒一个等待线程。  
  17. void signal()  
  18.   
  19. // 唤醒全部等待线程。  
  20. void signalAll()  

        Condition 接口有两个已知实现类:

Java代码    收藏代码
  1. AbstractQueuedLongSynchronizer.ConditionObject  
  2. AbstractQueuedSynchronizer.ConditionObject   

        AbstractQueuedLongSynchronizer 的属性和方法与 AbstractQueuedSynchronizer 彻底相同,但全部与状态相关的参数和结果都定义为 long 而不是 int。当建立须要 64 位状态的多级别锁和屏障等同步器时,此类颇有用。由于两个类基本相同,因此本文就只以 AbstractQueuedSynchronizer 的实现为例。

        在等待 Condition 时,容许发生“虚假唤醒”,这一般做为对基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,由于 Condition 应该老是在一个循环中被等待,并测试正被等待的状态声明。某个实现能够随意移除可能的虚假唤醒,但建议应用程序程序员老是假定这些虚假唤醒可能发生,所以老是在一个循环中等待。

 

        2、Lock

        Lock也是一个接口,它 实现提供了比使用 synchronized 方法和语句可得到的更普遍的锁定操做。此实现容许更灵活的结构,能够具备差异很大的属性,能够支持多个相关的 Condition 对象。 

        Lock 实现提供了比使用 synchronized 方法和语句可得到的更普遍的锁定操做。此实现容许更灵活的结构,能够具备差异很大的属性,能够支持多个相关的 Condition 对象。

        锁是控制多个线程对共享资源进行访问的工具。一般,锁提供了对共享资源的独占访问。一次只能有一个线程得到锁,对共享资源的全部访问都须要首先得到锁。不过,某些锁可能容许对共享资源并发访问,如 ReadWriteLock 的读取锁。

        synchronized 方法或语句的使用提供了对与每一个对象相关的隐式监视器锁的访问,但却强制全部锁获取和释放均要出如今一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与全部锁被获取时相同的词法范围内释放全部锁。 

        虽然 synchronized 方法和语句的范围机制使得使用监视器锁编程方便了不少,并且还帮助避免了不少涉及到锁的常见编程错误,但有时也须要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用 "hand-over-hand" 或 "chain locking":获取节点 A 的锁,而后再获取节点 B 的锁,而后释放 A 并获取 C,而后释放 B 并获取 D,依此类推。Lock 接口的实现容许锁在不一样的做用范围内获取和释放,并容许以任何顺序获取和释放多个锁,从而支持使用这种技术。 

        随着灵活性的增长,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数状况下,应该使用如下语句: 

Java代码    收藏代码
  1. Lock lock = 某实现;   
  2. //获取锁  
  3. lock.lock();  
  4. try {  
  5.     // 访问此锁保护的资源  
  6. finally {  
  7.     //释放锁  
  8.     lock.unlock();  
  9. }  

        锁定和取消锁定出如今不一样做用范围中时,必须谨慎地确保保持锁定时所执行的全部代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。 

        Lock 实现提供了使用 synchronized 方法和语句所没有的其余功能,包括提供了一个非块结构的获取锁尝试 (tryLock())、一个获取可中断锁的尝试 (lockInterruptibly()) 和一个获取超时失效锁的尝试 (tryLock(long, TimeUnit))。 

        Lock 类还能够提供与隐式监视器锁彻底不一样的行为和语义,如保证排序、非重入用法或死锁检测。若是某个实现提供了这样特殊的语义,则该实现必须对这些语义加以记录。 

        注意,Lock 实例只是普通的对象,其自己能够在 synchronized 语句中做为目标使用。获取 Lock 实例的监视器锁与调用该实例的任何 lock() 方法没有特别的关系。为了不混淆,建议除了在其自身的实现中以外,决不要以这种方式使用 Lock 实例。 

        除非另有说明,不然为任何参数传递 null 值都将致使抛出 NullPointerException。

 

        Lock接口有6个方法,分别是:

Java代码    收藏代码
  1. // 获取锁  
  2. void lock()   
  3.   
  4. // 若是当前线程未被中断,则获取锁  
  5. void lockInterruptibly()   
  6.   
  7. // 返回绑定到此 Lock 实例的新 Condition 实例  
  8. Condition newCondition()   
  9.   
  10. // 仅在调用时锁为空闲状态才获取该锁  
  11. boolean tryLock()   
  12.   
  13. // 若是锁在给定的等待时间内空闲,而且当前线程未被中断,则获取锁  
  14. boolean tryLock(long time, TimeUnit unit)   
  15.   
  16. // 释放锁  
  17. void unlock()   

        其中 lock与 unlock是最经常使用的方法,分别是获取与释放锁。

 

        如下是几个方法的详细解释及用法。

        1.newCondition() 方法

        返回绑定到此 Lock 实例的新 Condition 实例。 在等待条件前,锁必须由当前线程保持。调用 Condition.await() 将在等待前以原子方式释放锁,并在等待返回前从新获取锁。 实现时须要注意:Condition 实例的具体操做依赖于 Lock 实现,而且该实现必须对此加以记录。 

        2.lock() 方法

        获取锁,若是锁已被其余线程获取,则进行等待。

        若是锁不可用,出于线程调度目的,将禁用当前线程,而且在得到锁以前,该线程将一直处于休眠状态。以前已经说过若是采用Lock,必须主动去释放锁,而且在发生异常时,并不会自动释放锁。因此将释放锁的操做放在 try-finally 或 try-catch块中进行,以保证锁必定被被释放,防止死锁的发生。

        如下是一个示例来表现未释放锁的状况下的代码执行状况:

Java代码    收藏代码
  1. import java.util.concurrent.locks.Lock;  
  2. import java.util.concurrent.locks.ReentrantLock;  
  3.   
  4. public class LockThread {  
  5.     Lock lock = new ReentrantLock();  
  6.   
  7.     public void lock(String name) {  
  8.         // 获取锁  
  9.         lock.lock();  
  10.         try {  
  11.             System.out.println(name + " get the lock");  
  12.             // 访问此锁保护的资源  
  13.         } finally {  
  14.             // 释放锁  
  15.             //lock.unlock();  
  16.             //System.out.println(name + " release the lock");  
  17.         }  
  18.     }  
  19.   
  20.     public static void main(String[] args) {  
  21.         final LockThread lt = new LockThread();  
  22.         new Thread(new Runnable() {  
  23.   
  24.             public void run() {  
  25.                 lt.lock("A");  
  26.             }  
  27.         }).start();  
  28.         new Thread(new Runnable() {  
  29.   
  30.             public void run() {  
  31.                 lt.lock("B");  
  32.             }  
  33.         }).start();  
  34.     }  
  35.   
  36. }  
  37. //结果:  
  38. A get the lock  

        从结果就能够清晰的看到,A线程获取锁以后并无主动释放锁,而后 B线程开始执行,此时 B尝试获取锁,由于 A仍是锁的持有者,因此 B只好等待。

        将注释去掉,打印出来的正常结果应该是:

Java代码    收藏代码
  1. A get the lock  
  2. A release the lock  
  3. B get the lock  
  4. B release the lock  

        3.unlock() 方法

        释放锁。

        实现时须要注意:Lock 实现一般对哪一个线程能够释放锁施加了限制(一般只有锁的保持者能够释放它),若是违背了这个限制,可能会抛出(未经检查的)异常。该 Lock 实现必须对全部限制和异常类型进行记录。

        4.tryLock() 方法

        仅在调用时锁为空闲状态才获取该锁。 

        若是锁可用,则获取锁,并当即返回值 true。若是锁不可用,则此方法将当即返回值 false。 也就说这个方法不管如何都会当即返回,在拿不到锁时不会一直在那等待,这点与lock不一样。

        tryLock的典型使用方法以下:

Java代码    收藏代码
  1. Lock lock = 某锁实现;  
  2. //尝试获取锁  
  3. if (lock.tryLock()) {  
  4.     //若是成功则已获取锁  
  5.     try {  
  6.         // 操做被保护数据  
  7.     } finally {  
  8.         //释放锁  
  9.         lock.unlock();  
  10.     }  
  11. else {  
  12.     //获取锁失败  
  13. }  

        此用法可确保若是获取了锁,则会释放锁,若是未获取锁,则不会试图将其释放。

        5.tryLock(long time, TimeUnit unit) 方法

        若是锁在给定的等待时间内空闲,而且当前线程未被中断,则获取锁。 

        tryLock(long time, TimeUnit unit)方法和tryLock()方法是相似的,只不过区别在于这个方法在拿不到锁时会等待必定的时间,在时间期限以内若是还拿不到锁,就返回false。若是若是一开始拿到锁或者在等待期间内拿到了锁,则返回true。

        若是锁不可用,出于线程调度目的,将禁用当前线程,而且在发生如下三种状况之一前,该线程将一直处于休眠状态: 

        • 锁由当前线程得到;
        • 其余某个线程中断当前线程,而且支持对锁获取的中断;
        • 已超过指定的等待时间。

        若是当前线程: 

        • 在进入此方法时已经设置了该线程的中断状态;

        • 在获取锁时被中断,而且支持对锁获取的中断, 则将抛出 InterruptedException,并会清除当前线程的已中断状态。 

        若是超过了指定的等待时间,则将返回值 false。若是 time 小于等于 0,该方法将彻底不等待。

        实现时须要注意:在某些实现中可能没法中断锁获取,即便可能,该操做的开销也很大。程序员应该知道可能会发生这种状况。在这种状况下,该实现应该对此进行记录。 

        相对于普通方法返回而言,实现可能更喜欢响应某个中断,或者报告出现超时状况。 

        Lock 实现可能能够检测锁的错误用法,例如,某个调用可能致使死锁,在特定的环境中可能抛出(未经检查的)异常。该 Lock 实现必须对环境和异常类型进行记录。 

        6.lockInterruptibly() 方法

        若是当前线程未被中断,则获取锁。

        lockInterruptibly()方法比较特殊,当经过这个方法去获取锁时,若是线程正在等待获取锁,则这个线程可以响应中断,即中断线程的等待状态。也就使说,当两个线程同时经过 lock.lockInterruptibly()想获取某个锁时,倘若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用 threadB.interrupt()方法可以中断线程B的等待过程。

        若是锁可用,则获取锁,并当即返回。 若是锁不可用,出于线程调度目的,将禁用当前线程,而且在发生如下两种状况之一之前,该线程将一直处于休眠状态:

        • 锁由当前线程得到;

        • 其余某个线程中断当前线程,而且支持对锁获取的中断。

        若是当前线程: 

        • 在进入此方法时已经设置了该线程的中断状态;

        • 在获取锁时被中断,而且支持对锁获取的中断, 则将抛出 InterruptedException,并清除当前线程的已中断状态。

        实现时须要注意:在某些实现中可能没法中断锁获取,即便可能,该操做的开销也很大。程序员应该知道可能会发生这种状况。在这种状况下,该实现应该对此进行记录。 

        相对于普通方法返回而言,实现可能更喜欢响应某个中断。 Lock 实现可能能够检测锁的错误用法,例如,某个调用可能致使死锁,在特定的环境中可能抛出(未经检查的)异常。该 Lock 实现必须对环境和异常类型进行记录。 

        因为lockInterruptibly()的声明中抛出了异常,因此lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。因此 lockInterruptibly()通常这样使用:

Java代码    收藏代码
  1. try {  
  2.     // 获取锁  
  3.     lock.lockInterruptibly();  
  4.     // 访问此锁保护的资源  
  5. catch (InterruptedException e) {  
  6.     e.printStackTrace();  
  7. finally {  
  8.     // 释放锁  
  9.     lock.unlock();  
  10. }  

 

        3、ReadWriteLock

        ReadWriteLock 维护了一对相关的锁,一个用于只读操做,另外一个用于写入操做。只要没有 writer,读取锁能够由多个 reader 线程同时保持,而写入锁是独占的。

        全部 ReadWriteLock 实现都必须保证 writeLock 操做的内存同步效果也要保持与相关 readLock 的联系。也就是说,成功获取读锁的线程会看到写入锁以前版本所作的全部更新。

        与互斥锁相比,读-写锁容许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)能够修改共享数据,但在许多状况下,任何数量的线程能够同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所容许的并发性加强将带来更大的性能提升。在实践中,只有在多处理器上而且只在访问模式适用于共享数据时,才能彻底实现并发性加强。 

        与互斥锁相比,使用读-写锁可否提高性能则取决于读写操做期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或写入操做的线程数。例如,某个最初用数据填充而且以后不常常对其进行修改的 collection,由于常常对其进行搜索(好比搜索某种目录),因此这样的 collection 是使用读-写锁的理想候选者。可是,若是数据更新变得频繁,数据在大部分时间都被独占锁,这时,就算存在并发性加强,也是微不足道的。更进一步地说,若是读取操做所用时间过短,则读-写锁实现(它自己就比互斥锁复杂)的开销将成为主要的执行成本,在许多读-写锁实现仍然经过一小段代码将全部线程序列化时更是如此。最终,只有经过分析和测量,才能肯定应用程序是否适合使用读-写锁。 

        ReadWriteLock 接口很是简单,只有两个方法:

Java代码    收藏代码
  1. //返回用于读取操做的锁  
  2. Lock readLock()   
  3. //返回用于写入操做的锁  
  4. Lock writeLock()   

        尽管读-写锁的基本操做是直截了当的,但实现仍然必须做出许多决策,这些决策可能会影响给定应用程序中读-写锁的效果。这些策略的例子包括:

        • 在 writer 释放写入锁时,reader 和 writer 都处于等待状态,在这时要肯定是授予读取锁仍是授予写入锁。Writer 优先比较广泛,由于预期写入所需的时间较短而且不那么频繁。Reader 优先不太广泛,由于若是 reader 正如预期的那样频繁和持久,那么它将致使对于写入操做来讲较长的时延。公平或者“按次序”实现也是有可能的。 

        • 在 reader 处于活动状态而 writer 处于等待状态时,肯定是否向请求读取锁的 reader 授予读取锁。Reader 优先会无限期地延迟 writer,而 writer 优先会减小可能的并发。 

        • 肯定是否从新进入锁:可使用带有写入锁的线程从新获取它吗?能够在保持写入锁的同时获取读取锁吗?能够从新进入写入锁自己吗? 

        • 能够将写入锁在不容许其余 writer 干涉的状况降低级为读取锁吗?能够优先于其余等待的 reader 或 writer 将读取锁升级为写入锁吗? 

        虽然接口简单,可是实现须要考虑的问题仍是很是多的,在下一篇锁的实现中会再详细介绍读-写锁知识。

        以上就是三个接口的一些入门介绍,下一篇会学习这些接口的相关实现类。

相关文章
相关标签/搜索