并发编程原理与实战(十六)深入锁的演进,为什么有了synchronized还需要Lock?

前面两篇文章我们学习了产生线程安全问题的原因以及保证线程安全的方法,其中锁在保证线程安全的过程中起着关键性的作用。在《并发编程原理与实战(四)经典并发协同方式synchronized与wait+notify详解》和《并发编程原理与实战(十五)线程安全实现方法深度解析》这两篇文章中,我们对锁以及synchronized关键字已经有了一定的了解,synchronized是基于对象监视器实现的锁,那么有了synchronized这种锁了,为什么还需要基于Lock接口实现的锁?接下来我们来学习Lock相关的锁。

为什么需要Lock接口

官方列举了详细的说明,下面我们从Lock接口的官方说明中来分析为什么需要Lock接口。

/**
 * {@code Lock} implementations provide more extensive locking
 * operations than can be obtained using {@code synchronized} methods
 * and statements.  They allow more flexible structuring, may have
 * quite different properties, and may support multiple associated
 * {@link Condition} objects.
 */

Lock接口实现提供了比使用synchronized方法和语句所能获得的更广泛的锁操作。它们允许更灵活的结构设计,可能具备截然不同的特性,并且支持多个关联的Condition条件对象。

/**
 * 

A lock is a tool for controlling access to a shared resource by * multiple threads. Commonly, a lock provides exclusive access to a * shared resource: only one thread at a time can acquire the lock and * all access to the shared resource requires that the lock be * acquired first. However, some locks may allow concurrent access to * a shared resource, such as the read lock of a {@link ReadWriteLock}. */

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

/** 
 * 

The use of {@code synchronized} methods or statements provides * access to the implicit monitor lock associated with every object, but * forces all lock acquisition and release to occur in a block-structured way: * when multiple locks are acquired they must be released in the opposite * order, and all locks must be released in the same lexical scope in which * they were acquired. */

使用 synchronized方法或语句可访问与每个对象关联的隐式监视器锁,但它强制所有锁的获取和释放必须以块结构的方式发生。当获取多个锁时,必须以相反顺序释放,且所有锁必须在获取它们的同一词法作用域内释放。

/* 

While the scoping mechanism for {@code synchronized} methods * and statements makes it much easier to program with monitor locks, * and helps avoid many common programming errors involving locks, * there are occasions where you need to work with locks in a more * flexible way. For example, some algorithms for traversing * concurrently accessed data structures require the use of * "hand-over-hand" or "chain locking": you * acquire the lock of node A, then node B, then release A and acquire * C, then release B and acquire D and so on. Implementations of the * {@code Lock} interface enable the use of such techniques by * allowing a lock to be acquired and released in different scopes, * and allowing multiple locks to be acquired and released in any * order. */

尽管 synchronized方法和语句的作用域机制简化了监视器锁的编程,并有助于避免许多涉及锁的常见编程错误, 但在某些场景下,需要以更灵活的方式使用锁。例如,遍历并发访问数据结构的某些算法需要采用“交接连锁(hand-over-hand)”或“链锁(chain locking)”,先获取节点 A 的锁,再获取节点 B 的锁,然后释放 A 并获取 C,接着释放 B 再获取 D,依此类推。Lock接口的实现通过允许在不同作用域中获取和释放锁,并支持以任意顺序获取和释放多个锁。

/* 

With this increased flexibility comes additional * responsibility. The absence of block-structured locking removes the * automatic release of locks that occurs with {@code synchronized} * methods and statements. In most cases, the following idiom * should be used: * *

 {@code
 * Lock l = ...;
 * l.lock();
 * try {
 *   // access the resource protected by this lock
 * } finally {
 *   l.unlock();
 * }}
* * When locking and unlocking occur in different scopes, care must be * taken to ensure that all code that is executed while the lock is * held is protected by try-finally or try-catch to ensure that the * lock is released when necessary. */

这种灵活性的提升也带来了额外的责任,非块结构的锁机制移除了synchronized方法和语句中自动释放锁的特性。在大多数情况下,应使用以下惯用法:

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

当加锁与解锁发生在不同作用域时,必须确保锁持有期间执行的所有代码都被 try-finally 或 try-catch 保护,以保证锁在必要时被释放。

/* 

{@code Lock} implementations provide additional functionality * over the use of {@code synchronized} methods and statements by * providing a non-blocking attempt to acquire a lock ({@link * #tryLock()}), an attempt to acquire the lock that can be * interrupted ({@link #lockInterruptibly}, and an attempt to acquire * the lock that can timeout ({@link #tryLock(long, TimeUnit)}). */

Lock实现通过提供以下功能,在 synchronized方法和语句的基础上扩展了额外能力:

(1)非阻塞尝试获取锁(tryLock())

(2)可中断的锁获取(lockInterruptibly)

(3)支持超时的锁获取(tryLock(long, ))

/* 

A {@code Lock} class can also provide behavior and semantics * that is quite different from that of the implicit monitor lock, * such as guaranteed ordering, non-reentrant usage, or deadlock * detection. If an implementation provides such specialized semantics * then the implementation must document those semantics. */

Lock 类也可提供与隐式监视器锁完全不同的行为和语义,例如保证顺序性、不可重入用法或死锁检测。若实现提供此类特殊语义,则必须在文档中明确说明。

/* 

Note that {@code Lock} instances are just normal objects and can * themselves be used as the target in a {@code synchronized} statement. * Acquiring the * monitor lock of a {@code Lock} instance has no specified relationship * with invoking any of the {@link #lock} methods of that instance. * It is recommended that to avoid confusion you never use {@code Lock} * instances in this way, except within their own implementation. *

Except where noted, passing a {@code null} value for any * parameter will result in a {@link NullPointerException} being * thrown. */

需注意:Lock实例是普通对象,本身可在synchronized语句中作为目标使用,获取Lock实例的监视器锁与调用该实例的lock方法无必然关联。为避免混淆,建议除非在其自身实现中,否则不要以这种方式使用Lock实例。

除非特别说明,为任何参数传递null值将抛出NullPointerException异常。

内存同步

上一篇文章我们已经学习了内存模型的相关知识,Lock接口同样遵循8大原子操作的内存同步语义。

/* 

Memory Synchronization

* *

All {@code Lock} implementations must enforce the same * memory synchronization semantics as provided by the built-in monitor * lock, as described in * Chapter 17 of * The Java Language Specification: *

    *
  • A successful {@code lock} operation has the same memory * synchronization effects as a successful Lock action. *
  • A successful {@code unlock} operation has the same * memory synchronization effects as a successful Unlock action. *
* * Unsuccessful locking and unlocking operations, and reentrant * locking/unlocking operations, do not require any memory * synchronization effects. */

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

(1)成功的lock操作具有与成功的 Lock 动作(8大原子操作中的Lock)相同的内存同步效果。

(2)成功的unlock操作具有与成功的 Unlock 动作(8大原子操作中的UnLock)相同的内存同步效果。

失败的加锁/解锁操作以及可重入的加锁/解锁操作,不要求任何内存同步效果。

实现注意事项
/* 

Implementation Considerations

* *

The three forms of lock acquisition (interruptible, * non-interruptible, and timed) may differ in their performance * characteristics, ordering guarantees, or other implementation * qualities. Further, the ability to interrupt the ongoing * acquisition of a lock may not be available in a given {@code Lock} * class. Consequently, an implementation is not required to define * exactly the same guarantees or semantics for all three forms of * lock acquisition, nor is it required to support interruption of an * ongoing lock acquisition. An implementation is required to clearly * document the semantics and guarantees provided by each of the * locking methods. It must also obey the interruption semantics as * defined in this interface, to the extent that interruption of lock * acquisition is supported: which is either totally, or only on * method entry. */

锁获取的三种形式(可中断、不可中断和定时获取),可能在性能特征、顺序保证或其他实现质量上存在差异。此外,在给定的Lock实现类中,中断正在进行的锁获取操作的能力可能不被支持。因此,实现类无需为所有三种锁获取形式定义完全相同的保证或语义,也不要求必须支持中断正在进行的锁获取操作。但实现类必须明确记录每个加锁方法提供的语义和保证, 并在支持锁获取中断的范围内(无论完全支持或仅支持在方法入口处中断),遵守本接口定义的中断语义。

/* 

As interruption generally implies cancellation, and checks for * interruption are often infrequent, an implementation can favor responding * to an interrupt over normal method return. This is true even if it can be * shown that the interrupt occurred after another action may have unblocked * the thread. An implementation should document this behavior. */

由于中断通常意味着取消操作,且中断检查往往不频繁,实现类可优先响应中断而非正常方法返回,即使能证明中断发生在其他操作可能已解除线程阻塞之后,此原则依然适用。实现类应在其文档中说明此行为特性。

总结

最后,我们总结下synchronized和Lock的特性和使用场景。

特性 synchronized Lock(如 ReentrantLock
锁获取\释放方式 隐式 显式(lock()/unlock()
可中断锁 不支持 支持(lockInterruptibly()
超时获取锁 不支持 支持(tryLock(timeout)
公平锁 不支持(非公平) 支持(可配置)
多条件等待(Condition) 不支持(单一队列) 支持(多个 Condition
非阻塞尝试 不支持 支持(tryLock()
代码复杂度 高(需手动释放锁)

‌什么时候选择 Lock

  • 需要超时/中断功能时。
  • 需要公平的获取锁时。
  • 需绑定多个条件变量(如复杂生产者-消费者模型)。
  • 需尝试非阻塞获取锁(如避免死锁)。

‌什么时候选择 synchronized

  • 简单的同步场景(如单方法内的临界区)。
  • 追求代码简洁性和安全性(避免忘记解锁)。

你可能感兴趣的:(Java并发编程,java)