一、介绍

锁的实现提供了比使用 synchronized 方法和语句所能获得的更广泛的锁定操作。它们允许更灵活的结构化设计,可能具有非常不同的属性,并且可能支持多个关联的 Condition 对象。

锁是一种控制多个线程访问共享资源的工具。通常情况下,锁提供对共享资源的独占访问:一次只有一个线程可以获取锁,所有对共享资源的访问都需要先获取锁。然而,某些锁可能允许多个线程并发访问共享资源,例如 ReadWriteLock 的读锁。

使用 synchronized 方法或语句提供了对每个对象关联的隐式监视器锁的访问,但强制所有锁的获取和释放必须以块结构的方式进行:当获取多个锁时,它们必须按相反的顺序释放,并且所有锁必须在获取它们时的相同词法范围内释放。

虽然 synchronized 方法和语句的范围机制使得使用监视器锁编程变得更加容易,并有助于避免许多涉及锁的常见编程错误,但在某些情况下,你需要以更灵活的方式处理锁。例如,某些遍历并发访问数据结构的算法需要使用“手递手”或“链式锁定”:首先获取节点 A 的锁,然后是节点 B 的锁,然后释放 A 并获取 C,接着释放 B 并获取 D,依此类推。Lock 接口的实现通过允许在一个不同的范围内获取和释放锁,以及允许以任意顺序获取和释放多个锁,使得这种技术得以使用。

这种增加的灵活性带来了额外的责任。块结构锁定的缺失消除了与 synchronized 方法和语句相关的锁的自动释放。在大多数情况下,应使用以下惯用法:

1
2
3
4
5
6
7
8
Lock l = ...;
l.lock();
try {
// 访问由该锁保护的资源
// ......
} finally {
l.unlock();
}

当锁定和解锁发生在不同的范围内时,必须小心确保所有在持有锁期间执行的代码都受到 try-finallytry-catch 的保护,以确保在必要时释放锁。

Lock 的实现通过提供非阻塞尝试获取锁 tryLock()、可以中断的尝试获取锁 lockInterruptibly 和可以超时的尝试获取锁 tryLock(long, TimeUnit),提供了比使用 synchronized 方法和语句更多的功能。

一个 Lock 类还可以提供与隐式监视器锁截然不同的行为和语义,例如保证顺序、非可重入使用或死锁检测。如果实现提供了这样的专业语义,则实现必须记录这些语义。

请注意,Lock 实例只是普通对象,可以作为 synchronized 语句的目标使用。获取 Lock 实例的监视器锁与调用该实例的任何锁方法没有指定的关系。为了避免混淆,建议除在其实现内部外,不要以这种方式使用 Lock 实例。

除非另有说明,传递任何参数的 null 值将导致抛出 NullPointerException

1、内存同步

所有 Lock 的实现都必须强制执行与内置监视器锁相同的内存同步语义,这些语义在《Java语言规范》第17章中有描述:

  • 成功的 lock 操作具有与成功的 Lock 操作相同的内存同步效果。
  • 成功的 unlock 操作具有与成功的 Unlock 操作相同的内存同步效果。
  • 失败的 lock 操作和 unlock 操作,以及可重入的 lock 操作和 unlock 操作,不需要任何内存同步效果。

2、实现注意事项

三种形式的锁获取(可中断的、不可中断的和定时的)可能在性能特征、顺序保证或其他实现质量方面有所不同。

此外,给定的 Lock 类可能不支持中断正在进行的锁获取。因此,实现并不需要为所有三种形式的锁获取定义完全相同的保证或语义,也不需要支持中断正在进行的锁获取。实现必须清楚地记录每个锁方法提供的语义和保证。它还必须遵守此接口中定义的中断语义,前提是支持锁获取的中断:这可以是完全支持,也可以仅在方法进入时支持。

由于中断通常意味着取消,并且中断检查通常不频繁,因此实现可以优先响应中断而不是正常的方法返回。即使可以证明中断发生在另一操作可能解除线程阻塞之后,也是如此。实现应该记录这种行为。

二、源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Lock {

void lock();

void lockInterruptibly() throws InterruptedException;

boolean tryLock();

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

void unlock();

Condition newCondition();
}

1、lock 方法

该方法会获取锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到锁被获取。

实现注意事项

Lock 的实现可能能够检测锁的错误使用,例如会导致死锁的调用,并在这种情况下抛出一个(未检查的)异常。这些情况和异常类型必须由该锁的实现进行文档化。

2、lockInterruptibly 方法

获取锁,除非当前线程被中断。

如果锁可用,则立即获取锁并返回。 如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到发生以下两种情况之一:

  • 当前线程获取了锁
  • 其他线程中断了当前线程,并且锁获取的中断是受支持的

如果当前线程发生如下两种情况之一:

  • 在进入此方法时其中断状态已被设置
  • 在获取锁的过程中被中断,并且锁获取的中断是受支持的

则会抛出 InterruptedException 并清除当前线程的中断状态。

实现注意事项

在某些实现中,中断锁获取的能力可能不可行,即使可行也可能是一个昂贵的操作。程序员应该意识到这一点。实现应该记录这种情况。实现可以优先响应中断而不是正常的方法返回。

Lock 的实现可能能够检测锁的错误使用,例如会导致死锁的调用,并在这种情况下抛出一个(未检查的)异常。这些情况和异常类型必须由该锁的实现进行文档化。

方法抛出异常

InterruptedException,如果当前线程在获取锁时被中断(并且锁获取的中断是受支持的)

3、tryLock 方法

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

如果锁可用,则立即获取锁并返回 true。如果锁不可用,则此方法将立即返回 false

使用此方法的一个典型用法示例如下:

1
2
3
4
5
6
7
8
9
10
11
Lock lock = ...;
if (lock.tryLock()) {
try {
// 操作受保护的状态
// ......
} finally {
lock.unlock();
}
} else {
// 执行替代操作
}

这种用法确保了如果锁被获取,则会解锁;如果锁未被获取,则不会尝试解锁。

4、tryLock(long time, TimeUnit unit) 方法

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

如果锁可用,此方法将立即返回 true。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到以下三种情况之一发生:

  • 当前线程获取了锁
  • 其他线程中断了当前线程,并且锁获取的中断是受支持的
  • 指定的等待时间已过期

如果锁被获取,则返回 true。 如果当前线程发生以下两种情况之一:

  • 在进入此方法时其中断状态已被设置
  • 在获取锁的过程中被中断,并且锁获取的中断是受支持的

则会抛出 InterruptedException 并清除当前线程的中断状态。

如果指定的等待时间已过期,则返回 false。如果等待时间小于或等于零,则该方法将不会等待。

实现注意事项

在某些实现中,中断锁获取的能力可能不可行,即使可行也可能是一个昂贵的操作。程序员应该意识到这一点。实现应该记录这种情况。

实现可以优先响应中断而不是正常的方法返回,或者报告超时。

Lock 的实现可能能够检测锁的错误使用,例如会导致死锁的调用,并在这种情况下抛出一个(未检查的)异常。这些情况和异常类型必须由该锁的实现进行文档化。

5、unlock 方法

释放锁。

实现注意事项

Lock 的实现通常会对哪个线程可以释放锁施加限制(通常是只有锁的持有者可以释放它),并且如果违反了这些限制,可能会抛出一个(未检查的)异常。任何限制和异常类型必须由该锁的实现进行文档化。

6、newCondition 方法

返回一个新的绑定到此 Lock 实例的 Condition 实例。

在等待条件之前,当前线程必须持有该锁。调用 Condition.await() 方法会在等待前原子性地释放锁,并在等待返回前重新获取锁。

实现注意事项

Condition 实例的确切操作取决于锁的实现,并且必须由该实现进行文档化。

三、Java 中 Lock 的实现 ReentrantLock

相关链接

OB tags

#Java #多线程 #未完待续