Java Lock 和 Condition 接口

2017-06-27

Lock 接口

Lock 接口位于 java.util.concurrent.locks 包下,相比 synchronized 方法和语句,能提供更灵活的加锁解锁方法,并且支持创建多个 Condition 对象。

synchronized 方法或者语句能够让我们隐式访问每个对象的 monitor 锁,但会使得所有锁都要以一种阻塞的方式获取与释放:获取了多个锁后,这些锁必须严格按照加锁的顺序逆序释放。

使用 Lock 对象不仅可以上述情况,还可以非阻塞的获取锁(tryLock()),可中断的获取锁(lockInterruptibly()),以及在指定时间内获取锁(tryLock(long, TimeUnit)

尽管 Lock 接口带来了灵活性,但是我们现在需要显式地加锁和释放锁。在多数情况下,Lock 使用场景如下:

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

内存同步

所有 Lock 接口的实现必须执行与内置 monitor lock 相同的内存同步语义,如Java语言规范(17.4内存模型)中所述:

  • 一次成功的 lock 操作与一次成功的 Lock 动作有相同的内存同步效果
  • 一次成功的 unlock 操作与一次成功的 Unlock 动作有相同的内存同步效果

不成功的 locking 与 unlocking 操作,以及重入 locking/unlocking 操作,不要求任何的内存同步。

Lock API

Lock API 如下:

lock

void lock()

调用此方法将会获取锁。获得锁后,方法会立即返回;如果不能获得锁,那么当前线程将会阻塞,直到获得锁为止。

lockInterruptibly

void lockInterruptibly() throws InterruptedException

调用此方法时,如果当前线程不处于中断状态,将会获取锁。获得锁后,该方法会立即返回;如果不能获得锁,那么线程会一直阻塞直到出现以下两种情况:

  • 锁被当前线程获得,立即返回
  • 其他线程中断当前线程,并且锁支持中断

如果当前线程:

  • 在进入该方法时设置了中断状态
  • 在获取锁时被中断,并且锁支持中断

那么就会抛出 InterruptedException,同时清空当前线程的中断状态。

tryLock

boolean tryLock()

调用该方法,会非阻塞的获取锁。获得锁之后,方法立即返回 true;否则立即返回 false。

一个典型用法如下:

Lock lock = ...;
if(lock.tryLock()){
    try{
        // manipulate protected state
    } finally{
        lock.unlock();
} else{
    // perform alternative actions
}

tryLock 另一个重载方法如下:

boolean tryLock(long time, TimeUnit unit) throws InterruptedException

同样的,如果能获得锁,方法立即返回 true;否则线程会一直阻塞直到出现以下三种情况:

  • 锁被当前线程获得,立即返回 true
  • 其他线程中断了当前线程,并且锁支持了中断
  • 在指定时间内都没有获得锁,返回 false

如果当前线程:

  • 在进入该方法时设置了中断状态
  • 在获取锁时被中断,并且锁支持中断

那么就会抛出 InterruptedException,同时清空当前线程的中断状态。

unlock

void unlock()

调用此方法将会释放锁。

newCondition

Condition newCondition()

返回一个与当前 Lock 实例绑定的新的 Condition 实例。

如果当前锁实现不支持 conditions,那么就会抛出 UnsupportedOperationException。

Condition 接口

任意一个 Java 对象,都拥有一组 monitor 方法:wait()notify()notifyAll()。Lock 接口可以用于代替 synchronized 方法和语句,Condition 接口则可以代替对象 monitor 方法。Lock 与 Condition 配合使用可以实现等待/通知模式。

Conditions(也称为 condition queues 或者 condition varaibles)提供了一种机制,使得一个线程可以停止执行(to “wait”),直到某些状态条件为 true,被其他线程唤醒。因为状态信息要在多个线程之间被共享,所以一个 Condition 实例必须与某种形式的锁绑定。为了创建一个新的 Condition 实例,使用锁的 newCondition() 方法。使用 Condition 等待某个条件时,该 Condition 会原子地释放对应的锁并挂起当前线程,效果等价于 Object.wait。

Condition API

Condition API 可以分为两类:

  • 挂起线程
  • 唤醒线程

当前线程调用这两类方法时,都要求该线程持有与该 Condition 实例绑定的锁。

await

void await() throws InterruptedException

调用该方法会挂起当前线程,直到线程收到唤醒信号,或者被中断。

与该 Condition 绑定的锁将被原子地释放,同时当前线程会被挂起直到:

  • 其它线程调用了当前 Condition 的 $\mathtt {signal()}$,并且当前线程刚好被选中唤醒
  • 其它线程调用了当前 Condition 的 $\mathtt {signalAll()}$

  • 其它线程中断了当前线程,并且线程挂起支持中断
  • 出现 “spurious wakeup”

任意情况下,在方法返回前,当前线程必须再次获得该 Condition 绑定的锁。也就是说,当线程返回时,能保证线程持有锁。

如果当前线程:

  • 在进入该方法时设置了中断状态
  • 在等待时被中断并且线程挂起支持中断 就会抛出 InterruptedException 并且清除当前线程的中断状态。

awaitUninterruptibly

void awaitUniterruptibly()

awaitUniterruptibly 方法效果与 await 类似,不过该方法不支持响应中断。

awaitNanos

long awaitNanos(long nanosTimeout) throws InterruptedException

awaitNanos 是定时版 await ,方法返回值含义是剩余可等待时间,如果在 nanosTimeout 纳秒前返回,那么返回值等于(nanosTimeout - 实际等待耗时);如果超时返回,那么返回值小于等于0。

await

boolean await(long time, TimeUnit unit) throws InterruptedException

该方法等价于:

awaitNanos(unit.toNanos(time)) > 0

awaitUntil

boolean awaitUntil(Date deadline) throws InterruptedException

该方法的返回值可用于判断等待是否超过了 deadline,如果线程在 deadline 之前被唤醒,那么返回 true;否则返回 false。用法如下:

boolean aMethod(Date deadline){
    boolean stillWaiting = true;
    lock.lock();
    try{
        while(!conditionBeingWaitedFor()){
            if(!stillWaiting)
                return false;
            stillWaiting = theCondition.awaitUntil(deadline);
        }
    } finally{
        lock.unlock();
    }
}

signal

void signal()

调用该方法,将唤醒一个等待的线程。 如果有多个线程在等待当前 condition,那么会选择其中一个线程唤醒。该线程从 await 返回时必须持有与该 Condition 绑定的锁。

signalAll

void signalAll()

唤醒所有的等待线程。只有持有与该 Condition 绑定的锁,线程才能从 await 方法返回。

参考

  1. java.util.concurrent docs