云计算百科
云计算领域专业知识百科平台

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

上一篇我们学习了读写锁的相关知识,读写锁适合在读多写少的场景中使用,那么这可能会出现写锁饥饿的情况。当读操作非常频繁时,读锁可能被大量线程持续持有,写锁要获取,必须先等待所有当前的读锁释放,并且在它尝试获取期间,新的读锁请求也会因为“读写互斥”而被阻塞;如果已有读锁不断释放又有新读锁不断获取,写锁可能长时间甚至永远无法获得执行机会,从而导致等待的写线程出现饥饿。这是读写锁固有的缺点。

为了解决读写锁的这一问题,Java 8 引入了邮戳锁 StampedLock,核心改进在于引入了‌乐观读‌机制,改善读写锁读写互斥导致的‌写锁饥饿‌ 问题。本来就来学习StampedLock的相关知识。

StampedLock的三种模式

/**
* A capability-based lock with three modes for controlling read/write
* access. The state of a StampedLock consists of a version and mode.
* Lock acquisition methods return a stamp that represents and
* controls access with respect to a lock state; "try" versions of
* these methods may instead return the special value zero to
* represent failure to acquire access. Lock release and conversion
* methods require stamps as arguments, and fail if they do not match
* the state of the lock. The three modes are:
*/

一种基于能力的锁,提供三种模式来控制读/写访问。StampedLock 的状态由版本号和模式组成。锁获取方法返回一个邮戳(stamp),用于表示和控制对锁状态的访问;这些方法的 “try” 版本可能返回特殊值零来表示获取访问失败。锁释放和转换方法需要邮戳作为参数,如果邮戳与锁状态不匹配则会失败。三种模式如下:

/* <li><b>Writing.</b> Method {@link #writeLock} possibly blocks
* waiting for exclusive access, returning a stamp that can be used
* in method {@link #unlockWrite} to release the lock. Untimed and
* timed versions of {@code tryWriteLock} are also provided. When
* the lock is held in write mode, no read locks may be obtained,
* and all optimistic read validations will fail.
*/

写模式(Writing)

方法writeLock可能阻塞等待独占访问,返回一个邮戳,该邮戳可在方法unlockWrite中用于释放锁。同时也提供了tryWriteLock的非超时和超时版本。当锁处于写模式时,无法获取读锁,并且所有乐观读验证都将失败。

/*
* <li><b>Reading.</b> Method {@link #readLock} possibly blocks
* waiting for non-exclusive access, returning a stamp that can be
* used in method {@link #unlockRead} to release the lock. Untimed
* and timed versions of {@code tryReadLock} are also provided.
*/

读模式(Reading)

方法readLock可能阻塞等待非独占访问,返回一个邮戳,该邮戳可在方法unlockRead 中用于释放锁。同时也提供了tryReadLock的非超时和超时版本。

/*
* <li><b>Optimistic Reading.</b> Method {@link #tryOptimisticRead}
* returns a non-zero stamp only if the lock is not currently held in
* write mode. Method {@link #validate} returns true if the lock has not
* been acquired in write mode since obtaining a given stamp, in which
* case all actions prior to the most recent write lock release
* happen-before actions following the call to {@code tryOptimisticRead}.
* This mode can be thought of as an extremely weak version of a
* read-lock, that can be broken by a writer at any time. The use of
* optimistic read mode for short read-only code segments often reduces
* contention and improves throughput. However, its use is inherently
* fragile. Optimistic read sections should only read fields and hold
* them in local variables for later use after validation. Fields read
* while in optimistic read mode may be wildly inconsistent, so usage
* applies only when you are familiar enough with data representations to
* check consistency and/or repeatedly invoke method {@code validate()}.
* For example, such steps are typically required when first reading an
* object or array reference, and then accessing one of its fields,
* elements or methods.
*/

乐观读模式(Optimistic Reading)

方法 tryOptimisticRead仅当锁当前未被写模式持有时返回一个非零邮戳。方法validate 如果在获取给定邮戳以来锁未被写模式获取过则返回 true,在这种情况下,最近一次写锁释放之前发生的所有操作 happen-before(先行发生于)调用tryOptimisticRead 之后的操作。此模式可视为一种极其脆弱的读锁形式,随时可能被写线程中断。在短暂的只读代码段中使用乐观读模式通常可以减少争用并提高吞吐量。然而,其使用本质上具有脆弱性。乐观读代码段应仅读取字段并将其保存在局部变量中,以便在验证后使用。 在乐观读模式下读取的字段可能极不一致,因此仅适用于熟悉数据表示法并能检查一致性和/或重复调用validate() 方法的情况。例如,在首次读取对象或数组引用,然后访问其字段、元素或方法时,通常需要执行此类步骤。

备注:所谓的乐观读就是乐观的认为不会有写的发生,所以读操作无需加锁即可执行。反过来,悲观读即悲观的认为肯定有写发生,所以读操作必须加锁阻塞写锁的获取,阻止写操作访问数据,确保数据强一致性。

读写模式转换

/* <p>This class also supports methods that conditionally provide
* conversions across the three modes. For example, method {@link
* #tryConvertToWriteLock} attempts to "upgrade" a mode, returning
* a valid write stamp if (1) already in writing mode (2) in reading
* mode and there are no other readers or (3) in optimistic read mode
* and the lock is available. The forms of these methods are designed to
* help reduce some of the code bloat that otherwise occurs in
* retry-based designs.
*/

该类还支持在三种模式间进行条件性转换的方法。例如,方法 tryConvertToWriteLock 尝试将当前模式"升级"为写模式,在以下情况会返回有效的写邮戳:

(1) 当前已处于写模式;

(2) 处于读模式且无其他读线程持有锁;

(3) 处于乐观读模式且锁可用。

此类方法的设计旨在减少基于重试的设计中可能出现的代码冗余。

StampedLock的特性

/* <p>StampedLocks are designed for use as internal utilities in the
* development of thread-safe components. Their use relies on
* knowledge of the internal properties of the data, objects, and
* methods they are protecting. They are not reentrant, so locked
* bodies should not call other unknown methods that may try to
* re-acquire locks (although you may pass a stamp to other methods
* that can use or convert it). The use of read lock modes relies on
* the associated code sections being side-effect-free. Unvalidated
* optimistic read sections cannot call methods that are not known to
* tolerate potential inconsistencies. Stamps use finite
* representations, and are not cryptographically secure (i.e., a
* valid stamp may be guessable). Stamp values may recycle after (no
* sooner than) one year of continuous operation. A stamp held without
* use or validation for longer than this period may fail to validate
* correctly. StampedLocks are serializable, but always deserialize
* into initial unlocked state, so they are not useful for remote
* locking.
*/

StampedLock 设计用作开发线程安全组件的内部工具。其使用依赖于对受保护数据、对象和方法内部特性的认知。需要注意的关键限制包括:

不可重入:锁定代码块内不应调用可能尝试重新获取锁的其他未知方法(但可将邮戳传递给能使用或转换它的其他方法)。

读模式要求:使用读锁模式要求关联代码段无副作用。

乐观读限制:未经验证的乐观读代码段禁止调用无法容忍潜在数据不一致的方法。

邮戳特性:邮戳使用有限位数表示,不具备加密安全性(有效邮戳可能被猜测)。邮戳数值在连续运行一年后(最早一年后)可能复用。超过该期限未使用或验证的邮戳可能验证失败。

序列化行为:支持序列化,但反序列化后始终处于初始未锁定状态,因此不适用于远程锁场景。

/* <p>Like {@link java.util.concurrent.Semaphore Semaphore}, but unlike most
* {@link Lock} implementations, StampedLocks have no notion of ownership.
* Locks acquired in one thread can be released or converted in another.

* <p>The scheduling policy of StampedLock does not consistently
* prefer readers over writers or vice versa. All "try" methods are
* best-effort and do not necessarily conform to any scheduling or
* fairness policy. A zero return from any "try" method for acquiring
* or converting locks does not carry any information about the state
* of the lock; a subsequent invocation may succeed.
*
*/

不具备锁所有权:与 java.util.concurrent.Semaphore 类似,但不同于大多数Lock实现,StampedLock不具备所有权概念。在某个线程中获取的锁可在另一线程中释放或转换。StampedLock 的调度策略不会恒定地优先处理读线程或写线程。所有 “try” 开头的锁获取/转换方法 均为尽力而为(best-effort)机制,不保证遵循特定调度或公平性策略。这些 “try” 方法返回零值仅表示操作失败,不传递任何锁状态信息,后续重试调用仍可能成功。

/* <p>Because it supports coordinated usage across multiple lock
* modes, this class does not directly implement the {@link Lock} or
* {@link ReadWriteLock} interfaces. However, a StampedLock may be
* viewed {@link #asReadLock()}, {@link #asWriteLock()}, or {@link
* #asReadWriteLock()} in applications requiring only the associated
* set of functionality.
*/

非Lock接口实现类:由于支持跨多锁模式的协同使用,此类未直接实现Lock或ReadWriteLock接口。但在仅需部分关联功能的场景中,可通过视图方法 asReadLock()、asWriteLock()或asReadWriteLock()将StampedLock 转换为相应锁类型。

StampedLock的内存同步语义

/* <p><b>Memory Synchronization.</b> Methods with the effect of
* successfully locking in any mode have the same memory
* synchronization effects as a <em>Lock</em> action, as described in
* Chapter 17 of <cite>The Java Language Specification</cite>.
* Methods successfully unlocking in write mode have the same memory
* synchronization effects as an <em>Unlock</em> action. In optimistic
* read usages, actions prior to the most recent write mode unlock action
* are guaranteed to happen-before those following a tryOptimisticRead
* only if a later validate returns true; otherwise there is no guarantee
* that the reads between tryOptimisticRead and validate obtain a
* consistent snapshot.
*/

以任意模式成功锁定(locking)的方法具有与 ‌锁定操作‌(Lock action)相同的内存同步效果,如《Java语言规范》第17章所述。以写模式成功解锁(unlocking)的方法则具有与 ‌解锁操作‌(Unlock action)相同的内存同步效果。在乐观读(optimistic read)场景中,‌仅当后续 validate 返回 true 时‌,最近一次写模式解锁操作前的动作才保证 ‌happen-before‌ 那些位于 tryOptimisticRead 之后的动作;否则,无法保证 tryOptimisticRead 与 validate 之间的读取能获得一致性快照。

StampedLock核心API方法

最后总结下StampedLock核心API方法:

锁模式‌‌方法名‌‌返回值‌‌功能说明‌‌注意事项‌
‌写锁‌ writeLock() long stamp 获取独占写锁,阻塞直到成功。 必须通过 unlockWrite(stamp) 释放,否则导致死锁。
tryWriteLock() long stamp(0失败) 非阻塞尝试获取写锁,失败返回0。 适用于快速失败场景。
tryWriteLock(time, unit) long stamp(0超时) 定时尝试获取写锁,超时返回0。 时间单位需明确(如 TimeUnit.SECONDS)。
unlockWrite(stamp) void 释放写锁,需传入获取时返回的 stamp。 传入错误 stamp 会抛出异常。
‌悲观读锁‌ readLock() long stamp 获取共享读锁,阻塞直到成功。 必须通过 unlockRead(stamp) 释放。
tryReadLock() long stamp(0失败) 非阻塞尝试获取读锁,失败返回0。 与写锁互斥,失败时需处理重试或降级。
tryReadLock(time, unit) long stamp(0超时) 定时尝试获取读锁,超时返回0。 适用于避免长时间阻塞的场景。
unlockRead(stamp) void 释放读锁,需传入获取时的 stamp。 必须与获取锁的 stamp 匹配。
‌乐观读‌ tryOptimisticRead() long stamp(0失败) 尝试乐观读,返回非0 stamp 表示当前无写锁。 无需显式释放,但需调用 validate(stamp) 验证数据一致性。
validate(stamp) boolean 检查乐观读期间是否有写操作(true 表示数据有效)。 若返回 false,需升级为悲观读或重试。
‌锁转换‌ tryConvertToWriteLock(stamp) long(0失败) 尝试将读锁升级为写锁,返回新 stamp 或0。 升级失败需手动释放原锁并重新获取写锁。
tryConvertToReadLock(stamp) long(0失败) 尝试将写锁降级为读锁,返回新 stamp 或0。 降级失败需释放写锁后重新获取读锁。
‌通用方法‌ unlock(stamp) void 通用释放锁,自动识别锁类型(读/写)。 仅适用于锁转换后的场景,直接替代 unlockRead/unlockWrite 可能不安全。
isWriteLocked() boolean 检查当前是否为写锁模式。 主要用于调试和监控。
isReadLocked() boolean 检查当前是否为读锁模式(悲观读)。 不反映乐观读状态。
赞(0)
未经允许不得转载:网硕互联帮助中心 » 并发编程原理与实战(二十二)深入锁的演进,为什么有了ReadWriteLock还需要StampedLock?
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!