当前位置: 首页> 财经> 金融 > 【Java】有关StampedLock的笔记+StampedLock的队列与AQS有什么区别

【Java】有关StampedLock的笔记+StampedLock的队列与AQS有什么区别

时间:2025/8/29 4:57:33来源:https://blog.csdn.net/Hansdas/article/details/139144146 浏览次数:0次

文章目录

  • 0. Why `StampedLock`
  • 1. `StampedLock`概念
    • 1.1 三种访问模式
    • 1.2 stamp 印戳
  • 2. `StampedLock`的使用
    • 2.1 读写锁的获取与使用
    • 2.2 乐观读
      • 2.2.1 为什么乐观读要升级为悲观读锁而不像CAS一样自旋?
  • 3. `StampLock`的实现:state+队列
    • state
    • 队列
  • 注意事项
  • Reference

0. Why StampedLock

ReadWriteLock 适合读多写少的高并发场景,但读写互斥(读时不允许写,写时不允许读)

  • 写饥饿问题:一个线程”写“的时候,其它线程不能读也不能写(阻塞该线程,会影响性能)

StampedLock :在读的时候如果发生了写,应该通过重试的方式来获取新的值,而不应该阻塞写操作(乐观读,无锁编程,类似CAS的思想)

  • 支持多个线程申请乐观读的同时,还允许一个线程申请写锁
  • 避免了写饥饿,适合读多写少且写线程非常少的场景(比ReadWriteLock更快):
    • 没有写只有读时:不用加锁读
    • 写操作后:加锁读
  • 底层并不是基于AQS的
  • 读写都不可重入

1. StampedLock概念

1.1 三种访问模式

  • 乐观读
    • 在多个线程读取共享变量时,允许一个线程对共享变量进行写操作
    • 不加锁,直接操作数据:
      • 操作数据前也不CAS设置锁状态,仅位运算测试stamp。
      • 获取该 stamp 后在具体操作数据前还需要调用validate 方法验证该 stamp 是否己经不可用
  • 读锁(悲观)与写锁:
    • ReadWriteLock 类似:只允许一个线程获取写锁(独占),写锁和读锁也是互斥的
    • ReadWriteLock 不同:StampedLock获取读锁或写锁成功后,都会返回一个Long,释放锁时需要传入这个Long。且StampedLock不可重入

1.2 stamp 印戳

StampedLock 获取锁后,返回一个long类型的stamp,通过位运算来确定锁的状态

  • 结果为0:如果当前没有线程持有写锁
  • 结果不为0:如果当前有线程持有写锁,只能悲观读写

2. StampedLock的使用

2.1 读写锁的获取与使用

public class StampedLockDemo{//创建StampedLock锁对象public StampedLock stampedLock = new StampedLock();//获取、释放读锁public void testGetAndReleaseReadLock(){long stamp = stampedLock.readLock();try{//执行获取读锁后的业务逻辑}finally{//释放锁stampedLock.unlockRead(stamp);}}//获取、释放写锁public void testGetAndReleaseWriteLock(){long stamp = stampedLock.writeLock();try{//执行获取写锁后的业务逻辑。}finally{//释放锁stampedLock.unlockWrite(stamp);}}
}

2.2 乐观读

如果在执行乐观读操作时,另外的线程对共享变量进行了写操作,则会把乐观读升级为悲观读锁,比如下面的distanceFromOrigin

package org.example.LockEx;import java.util.concurrent.locks.StampedLock;public class StampedLockEx {private double x, y;private final StampedLock sl = new StampedLock();void move(double deltaX, double deltaY) {// 写锁 exclusivelong stamp = sl.writeLock();try{x += deltaX;y += deltaY;}finally {sl.unlockWrite(stamp);}}double distanceFromOrigin() {// 获取乐观读锁,此时可以有另一个线程持有读锁long stamp = sl.tryOptimisticRead();double currentX = x, currentY = y;if (!sl.validate(stamp)) {// 获取乐观读锁后,如果stamp失效// 即被持有写锁的线程修改了,validate返回false// 乐观读锁转为悲观读锁stamp = sl.readLock();try {// 悲观读锁跟写锁是互斥的,重新读一遍资源,后面就不会被写锁修改了currentX = x;currentY = y;}finally {// 释放悲观读锁sl.unlock(stamp);}}return Math.sqrt(currentX * currentX + currentY * currentY);}void moveIfAtOrigin(double newX, double newY) { // upgrade// Could instead start with optimistic, not read modelong stamp = sl.readLock();try {while (x == 0.0 && y == 0.0) {long ws = sl.tryConvertToWriteLock(stamp);if (ws != 0L) {stamp = ws;x = newX;y = newY;break;}else {sl.unlockRead(stamp);stamp = sl.writeLock();}}} finally {sl.unlock(stamp);}}}

2.2.1 为什么乐观读要升级为悲观读锁而不像CAS一样自旋?

乐观读期间遇到另一个线程写时会升级为悲观读锁,此时读写互斥

因为如果不升级,程序会在循环中反复执行乐观读,直到乐观读期间没有线程执行写操作,类似CAS自旋。

但是前面说过,StampLock适合读多写少且写线程较少的场景,它性能之所以比ReadWriteLock好,就是因为大量的读操作线程可以通过乐观读的方式无锁进行。因此可能会有大量乐观读的线程,如果全都在循环执行,会消耗大量的CPU资源

  • 这里不是说StampLock 里不用CAS,内部关于stamp的操作(比如我们获取锁时返回stamp的方法)都涉及到CAS

3. StampLock的实现:state+队列

state

核心就是使用戳记(stamp)的方式来标记数据的版本,乐观读的时候就是对比stamp来保证线程安全,而获取锁的方法返回的stamp则是通过state属性位运算得到的

state是一个int,默认值是256(1 0000 0000),共32位:

  • 前24位表示版本号
  • 低8位表示锁
    • 低8位的第1位表示是否为写锁:1表示写锁、0表示没有写锁
    • 剩下7位表示悲观读锁的个数

队列

StampedLock内部是基于CLH锁实现的,CLH是一种自旋锁,且是公平的(保证FIFO,不会有锁饥饿)

注意StampedLock 并不通过AQS实现,但是AQS内部的队列也是CLH的变体,所以还是有很多类似的地方

StampLock 的CLH就是维护了一个线程的等待队列,所有申请锁但是没有成功的线程都会包装成一个节点存入队列(类似AQS)。AQS中,每个节点有各种状态(SIGNAL、CONDITION等等),而CLH为每个节点维护了一个locked 属性,true代表获取到锁,false表示成功释放锁

当一个线程试图获得锁时,取得等待队列的尾部节点作为其前序节点,并循环判断前一节点是否成功释放锁:

while (pred.locked) {//省略操作 
}

这也是跟AQS不同的地方,AQS中排队等待获取锁的线程节点是前一线程释放锁后unpark() 唤醒的。而StampLock 的CLH中,等待的节点是一直在询问前一节点是否释放锁

注意事项

  • StampedLock不支持重入:因此不能嵌套使用

  • StampedLock不支持条件变量

  • StampedLock使用不当会导致CPU飙升:如果某个线程阻塞在StampedLockreadLock()或者writeLock()方法上时,此时调用阻塞线程的interrupt()方法中断线程,会导致CPU飙升到100%:

    public void testStampedLock() throws Exception{final StampedLock lock = new StampedLock();Thread thread01 = new Thread(()->{// 获取写锁lock.writeLock();// 永远阻塞在此处,不释放写锁LockSupport.park();});thread01.start();// 保证thread01获取写锁Thread.sleep(100);Thread thread02 = new Thread(()->//阻塞在悲观读锁lock.readLock());thread02.start();// 保证T2阻塞在读锁Thread.sleep(100);//中断线程thread02//会导致线程thread02所在CPU飙升thread02.interrupt();thread02.join();
    }
    

Reference

【高并发】高并发场景下一种比读写锁更快的锁
StampedLock(印戳锁)详解

关键字:【Java】有关StampedLock的笔记+StampedLock的队列与AQS有什么区别

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: