Java 中的 Lock 接口及实现
一、介绍
锁的实现提供了比使用 synchronized
方法和语句所能获得的更广泛的锁定操作。它们允许更灵活的结构化设计,可能具有非常不同的属性,并且可能支持多个关联的 Condition
对象。
锁是一种控制多个线程访问共享资源的工具。通常情况下,锁提供对共享资源的独占访问:一次只有一个线程可以获取锁,所有对共享资源的访问都需要先获取锁。然而,某些锁可能允许多个线程并发访问共享资源,例如 ReadWriteLock
的读锁。
使用 synchronized
方法或语句提供了对每个对象关联的隐式监视器锁的访问,但强制所有锁的获取和释放必须以块结构的方式进行:当获取多个锁时,它们必须按相反的顺序释放,并且所有锁必须在获取它们时的相同词法范围内释放。
虽然 synchronized
方法和语句的范围机制使得使用监视器锁编程变得更加容易,并有助于避免许多涉及锁的常见编程错误,但在某些情况下,你需要以更灵活的方式处理锁。例如,某些遍历并发访问数据结构的算法需要使用“手递手”或“链式锁定”:首先获取节点 A 的锁,然后是节点 B 的锁,然后释放 A 并获取 C,接着释放 B 并获取 D,依此类推。Lock
接口的实现通过允许在一个不同的范围内获取和释放锁,以及允许以任意顺序获取和释放多个锁,使得这种技术得以使用。
这种增加的灵活性带来了额外的责任。块结构锁定的缺失消除了与 synchronized
方法和语句相关的锁的自动释放。在大多数情况下,应使用以下惯用法:
1 | Lock l = ...; |
当锁定和解锁发生在不同的范围内时,必须小心确保所有在持有锁期间执行的代码都受到 try-finally
或 try-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 | public interface Lock { |
1、lock 方法
该方法会获取锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到锁被获取。
实现注意事项
Lock
的实现可能能够检测锁的错误使用,例如会导致死锁的调用,并在这种情况下抛出一个(未检查的)异常。这些情况和异常类型必须由该锁的实现进行文档化。
2、lockInterruptibly 方法
获取锁,除非当前线程被中断。
如果锁可用,则立即获取锁并返回。 如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到发生以下两种情况之一:
- 当前线程获取了锁
- 其他线程中断了当前线程,并且锁获取的中断是受支持的
如果当前线程发生如下两种情况之一:
- 在进入此方法时其中断状态已被设置
- 在获取锁的过程中被中断,并且锁获取的中断是受支持的
则会抛出 InterruptedException
并清除当前线程的中断状态。
实现注意事项
在某些实现中,中断锁获取的能力可能不可行,即使可行也可能是一个昂贵的操作。程序员应该意识到这一点。实现应该记录这种情况。实现可以优先响应中断而不是正常的方法返回。
Lock
的实现可能能够检测锁的错误使用,例如会导致死锁的调用,并在这种情况下抛出一个(未检查的)异常。这些情况和异常类型必须由该锁的实现进行文档化。
方法抛出异常
InterruptedException
,如果当前线程在获取锁时被中断(并且锁获取的中断是受支持的)
3、tryLock 方法
仅在调用时锁为空闲状态时获取锁。
如果锁可用,则立即获取锁并返回 true
。如果锁不可用,则此方法将立即返回 false
。
使用此方法的一个典型用法示例如下:
1 | Lock lock = ...; |
这种用法确保了如果锁被获取,则会解锁;如果锁未被获取,则不会尝试解锁。
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 links
OB tags
#Java #多线程 #未完待续