Java8读写锁的改进:StampedLock

StampedLock是Java8中新增的锁机制,它是读写锁的改进版。它提供了一种乐观的读策略,这种锁类似无锁操作,完全不会阻塞写线程。

虽然读写锁分离了读和写的操作,让读与读之间可以完全并发,但读与写仍然是冲突的。读锁会阻塞写锁,它使用的是悲观锁策略,当有大量的读线程时也可能引起线程饥饿。

StampedLock的内部实现,使用的是类似CAS的死循环反复尝试的策略。在它挂起线程时,使用的是Unsafe.park(),park()在线程中断时会直接返回。StampedLock在死循环逻辑中国,没有处理有关中断的逻辑。这就会导致阻塞在park()上的线程被中断后,会再次进入循环。当退出条件不满足时,会发生疯狂占用CPU的情况。以下示例模拟类似的情况:

StampedLock的内部实现是基于CLH锁的。CLH锁是一种自旋锁,它保证没有饥饿发生,并保证FIFO(先进先出)的服务顺序。

CLH锁的基本思想:锁维护一个等待线程队列,所有申请锁没有成功的线程线程都记录在这个队列中。每个节点(一个节点代表一个线程)保存一个标记位(locked),用于判断当前线程释放已经释放锁。

当一个线程试图获得锁时,取得当前等待队列的尾部节点作为其前序节点,并使用类似如下代码判断前序节点释放已经成功释放锁:while(pred.locked){}。只要前序节点(pred)没有释放锁,则当前线程不能继续执行,会自旋等待。反之则当前线程可以继续执行。释放锁时也遵循此逻辑,线程会将自身节点的locked位置标记为false,后续等待的线程就可以继续执行了。

在StampedLock内部会维护一个等待链表队列WNode:

WNode定义

WNode为链表的基本元素,每个WNode表示一个等待线程,whead和wtail分别指向等待链表的头部和尾部。

state定义

state表示当前锁的状态,它是long类型,长度有64位,倒数第8位表示写锁状态,如果该位为1表示当前由写锁占用。

对于一次乐观读操作会执行如下操作:

tryOptimisticRead()定义

一次成功的乐观读必须保证当前锁没有写锁占用,其中WBIT用来获取写锁状态位,值为0x80,。如果成功则返回当前state的值(末尾7位清零,末尾7位表示当前正在读取的线程数量)。

如果在乐观读后,有线程申请了写锁,则state的状态就会改变:

writeLock()定义

U.compareAndSwapLong(this, STATE, s, next = s +WBIT))方法设置写锁位为1(通过加WBIT(0x80))。在乐观锁确认(volatile())时,就会发现这个改动,导致乐观锁失效。

volatile()定义

volatile()比较当前stamp和发生乐观锁时取得的stamp是否一致,如果不一致则乐观锁失败。乐观锁失败后,可以提升锁级别,使用悲观读锁:

readLock()定义

readLock()会尝试设置state状态,它会将state加1,用于统计读线程的数量。如果失败则进入acquireRead()进行二次尝试获取锁。在acquireRead()中,线程会在不同条件下进行若干次自旋,试图通过CAS操作获取锁。如果自旋失败,会启用CLH队列将自己加入到队列中,之后再进行自旋,如果发现自己成功获取锁,则会进一步把自己cowait队列中的读线程全部激活(使用Unsafe.unpark()方法)。如果最终无法成功获取锁,则会使用Unsafe.park()方法挂起当前线程。

unlockWrite()定义

unlockWrite()也是通过自旋尝试、加入等待队列、最终Unsafe.park()挂起线程的逻辑进行的,但其释放锁和加锁动作相反。

state = (stamp +=WBIT) == 0L ? ORIGIN : stamp; //将写标记位清零,如果state溢出,则退回到初始值

如果等待队列不为空,则从等待队列中激活一个线程继续执行。

release()定义


--参考文献《实战Java高并发程序设计》

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容