一. 介绍



我在《AQS(5)——ConditionObject》这篇文章中曾提过管程这个概念,它能够解决并发编程领域的两大核心问题

  1. 互斥:同一时刻只能有一个线程访问共享资源
  2. 同步:线程之间的通信协作

而Java在1.5版本之前,是通过synchronized来实现。到了Java 1.5版本,Java大神Doug Lea的又在Java并发包增加了Lock和Condition两接口来解决这两个问题

为啥synchronized实现了管程,还要添加这两接口重复实现呢?答案很容易猜到,就是synchronized有缺陷,针对互斥与同步都存在缺陷:

  1. 无法解决死锁问题
  2. 最多使用一个条件变量

接下来我们就来看看Lock与Condition接口是如何解决这两个问题的




二. Lock接口



2.1 介绍

我们知道,只有以下四个条件都发生时才会出现死锁:

  1. 互斥:共享资源只能被一个线程占用
  2. 不可抢占:其他线程不能强行另一个线程占有的资源
  3. 占有且等待:线程T1已经取得共享资源X时,在等待共享资源Y时候,不释放共享资源X
  4. 循环等待:线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源

所以,我们只要破坏其中一个,就可以成功避免死锁的发生,需要注意的是,互斥这个条件我们没办法破坏,因为我们用锁就是为了互斥,而其他三个是可以破坏掉。

Lock是通过下面三个方式来破坏掉 "不可抢占"这个条件的:

  1. 非阻塞获取锁:尝试获取锁,如果失败了就立刻返回失败,这样就可以释放已经持有的锁
  2. 响应中断:如果发生死锁后,此线程被其他线程中断,则会释放锁,解除死锁
  3. 支持超时:一段时间内获取不到锁,就返回失败,这样就可以释放之前已经持有的锁

还需要额外注意的一点,当我们使用内置锁时,是不需要考虑释放锁的,而Lock作为一种显示锁,是需要手动释放的。我们一般通过try-finally或try-catch保护锁定时执行的所有代码,并确保在必要时释放锁定,作者也给我们提供了比较常用的使用的方式

Lock l = ...;
	 l.lock();
 	 try {
   // access the resource protected by this lock
	 } finally {
  	 l.unlock();
	 }

接下来我们具体来看看接口吧

2.2 源码解读
public interface Lock {
	/**
	  阻塞获取锁,不响应中断,如果获取不到,则当前线程将进入休眠状态,直到获得锁为止。
	*/
    void lock();

  	/**
	  阻塞获取锁,响应中断,如果出现以下两种情况将抛出异常
	  1.调用该方法时,此线程中断标志位被设置为true
	  2.获取锁的过程中此线程被中断,并且获取锁的实现会响应中断
	 */
    void lockInterruptibly() throws InterruptedException;
  
    /**
      非阻塞获取锁,不管成功还是失败,都会立刻返回结果,成功了返回true,失败了返回false
      该方法的典型用法是:
  	   Lock lock = ...;
       if (lock.tryLock()) {
          try {
             // manipulate protected state
          } finally {
            lock.unlock();
          }
        } else {
         // perform alternative actions
        }}
       这种用法可确保在获取锁后将其解锁,并且在未获取锁时不会尝试解锁。
     */
    boolean tryLock();
 
    /**
      带超时时间且响应中断的获取锁,如果获取锁成功,则返回true,获取不到则会休眠,直到下面三个条件满足
      1.当前线程获取到锁
      2.其他线程中断了当前线程,并且获取锁的实现支持中断
      3.设置的超时事件到了
      而抛出异常的情况与lockInterruptibly一致
      当异常抛出后中断标志位会被清除,且超时时间到了,当前线程还没有获得锁,则会直接返回false
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
   
    /**
      没啥好说,只有拥有锁的线程才能释放锁
     */
    void unlock();

    /**
      返回绑定到此Lock实例的新Condition实例。
      在等待该条件之前,该锁必须由当前线程持有。调用Condition.await()会在等待之前自动释放锁,并在等待返回之前重新获取该锁。
      我们再下一小节再详细说说Condition接口
     */
    Condition newCondition();
}




三. Condition接口



3.1 介绍

Lock解决了synchronized不能处理死锁的问题,并对功能进行了扩展,而Condition接口就是解决synchronized只能有一个条件变量的缺点。

同一个synchronize内置锁只对应一个wait set,即当线程调用wait方法时,会把当前线程放入到同一个wait set中,当我们需要根据某些特定的条件来唤醒符合条件的线程时,我们只能先从wait set里唤醒一个线程后,再看是否符合条件,如果不符合条件,则需要将此线程继续wait,然后再去wait set中获取下一个线程再判断是否满足条件。这样会导致许多无意义的cpu开销。

所以Condition就是为了解决上面问题存在的。每一个锁都会对应多个Condition,每个Condtition都有一个容器来保存相应的等待线程,拿到锁的线程想唤醒某个等待特定条件的线程时,只需要去唤醒对应Condition容器中的线程即可。



3.2 源码解读
public interface Condition {

    /**
      使当前线程等待,并响应中断。当当前线程进入休眠状态后,如果发生以下四种情况将会被唤醒:
	  1.其他一些线程对此条件调用signal方法,而当前线程恰好被选择为要唤醒的线程;
	  2.其他一些线程对此条件调用signalAll方法
	  3.其他一些线程中断当前线程,并支持中断线程挂起
	  4.发生“虚假唤醒”。
     */
    void await() throws InterruptedException;

    /**
	  使当前线程等待,并不响应中断。只有以下三种情况才会被唤醒
	  1.其他一些线程对此条件调用signal方法,而当前线程恰好被选择为要唤醒的线程;
	  2.其他一些线程对此条件调用signalAll方法
	  3.发生“虚假唤醒”。
     */
    void awaitUninterruptibly();

    /**
      使当前线程等待,响应中断,且可以指定超时事件。发生以下五种情况之一将会被唤醒:
      1.其他一些线程为此条件调用signal方法,而当前线程恰好被选择为要唤醒的线程;
      2.其他一些线程为此条件调用signalAll方法;
      3.其他一些线程中断当前线程,并且支持中断线程挂起;
      4.经过指定的等待时间;
      5.发生“虚假唤醒”。
     */
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    /**
	  与awaitNanos类似,时间单位不同
     */
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    /**
	  与awaitNanos类似,只不过超时时间是截止时间
     */
    boolean awaitUntil(Date deadline) throws InterruptedException;

    /**
	  唤醒一个等待线程
     */
    void signal();

    /**
	  唤醒所有等待线程
     */
    void signalAll();
}

前文《AQS(5)——ConditionObject》中已经对实现了Condition接口的ConditionObject类做了详细的分析,如有需要,大家可以移步至那篇文章进行学习

下篇文章我将会分析实现了Lock接口的ReentrentLock类。