【java并发工具类-互斥】 StampedLock(比读写锁性能更高的锁)

2. StampedLock

2.1 StampedLock支持的三种锁模式

这三种模式分别是:写锁,悲观读锁,乐观读

  • 其中,它的写锁,悲观读锁和ReadWriteLock的写锁,读锁语义类似:都是允许多个线程同时获得悲观读锁,只允许一个线程获得写锁,同时写锁和读锁是互斥的。不同的是:StampedLock里写锁,悲观读锁在加锁成功后,都会返回一个stamp;然后解锁时,都需要传入这个stamp。相关代码如下:
final StampedLock sl =  new StampedLock();  
long stamp = sl.readLock();		// 获取/释放悲观读锁示意代码
try {
  //省略业务相关代码
} finally {
  sl.unlockRead(stamp);
}

long stamp = sl.writeLock();// 获取/释放写锁示意代码
try {
  //省略业务相关代码
} finally {
  sl.unlockWrite(stamp);
}
  • 乐观读:StampedLock之所以比ReadWriteLock性能好,关键就是StampedLock支持乐观读(乐观读操作是无锁的)!
    ReadWriteLock支持多个线程读,但是当多个线程读时,所有的写线程都被阻塞,而StampedLock提供的乐观读,当多个线程在读时,允许一个线程获得写锁!
2.2 StampedLock乐观读原理

下面这段代码是出自 Java SDK 官方示例,并略做了修改。

class Point {
  private int x, y;
  final StampedLock sl = new StampedLock();
  int distanceFromOrigin() { //计算到原点的距离  
    // 乐观读
    long stamp = sl.tryOptimisticRead();  
    int curX = x, curY = y; // 读入局部变量, 读的过程数据可能被修改
    //判断执行读操作期间,是否存在写操作,如果存在,则sl.validate返回false
    if (!sl.validate(stamp)){
      // 升级为悲观读锁,
      stamp = sl.readLock();
      try {
        curX = x;
        curY = y;
      } finally {
        //释放悲观读锁
        sl.unlockRead(stamp);
      }
    }
    return Math.sqrt(
      curX * curX + curY * curY);
  }
}

上面的代码中,如果执行乐观读期间存在写操作,会把乐观读升级为悲观读锁,否则需要一个循环反复执行乐观读,直到期间没有写操作,循环会浪费cpu资源,所以升级为悲观读锁,合情合理!

注意:在上面代码中validate校验期间没有写操作,但是如果在执行Math.sqrt之前,校验之后,进行写操作,还是不能及时判断数据的同步,只能保证正确性和一致性。这是我有点不明白的地方,如果有哪位老师明白的,希望可以写在评论区留言。

其实在提到乐观读,你可能会想到数据库事务MVCC(多版本并发控制),其实两者有点相似,按照MVCC原理,数据的每次修改都对应一个版本号,不存在只修改数据或者版本号。
在数据库事务开启的时候,会给数据库打一个快照,以后再该事务中所有的读写操作都是基于这个快照的,当提交事务的时候,也就时写入数据库数据时,会校验写入数据版本是否与读数据时的版本号一致,如果一致,就是所有读写过的数据在该事务执行期间没有发生变化,就提交数据,如果不一致,发生变化了,说明在该事务执行期间,其他事务提交了,产生冲突,不能提交。

乐观读的想法是,没事,没有写操作,校验一下是否存在写操作,没有,操作数据;有,升级悲观锁。
MVCC的想法是,没事,没有写操作,复制一份,先修改复制的,在写入数据库时,(检验版本看是否存在其他写操作,有,不能提交事务,没有,提交事务。)原子操作

2.3 StampedLock注意事项

对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是 StampedLock 的功能仅仅是 ReadWriteLock 的子集,在使用的时候,还是有几个地方需要注意一下。

  1. StampedLock 在命名上并没有增加 Reentrant,所以,StampedLock 不支持重入。
  2. 另外,StampedLock 的悲观读锁、写锁都不支持条件变量,这个也需要你注意。
  3. 如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。所以,使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。这个规则一定要记清楚。
    例如下面的代码示例(中断阻塞的readLock导致cpu飙升):
final StampedLock lock = new StampedLock();
Thread T1 = new Thread(()->{
  // 获取写锁
  lock.writeLock();
  LockSupport.park(); // 永远阻塞在此处,不释放写锁
});
T1.start();
// 保证T1获取写锁
Thread.sleep(100);
Thread T2 = new Thread(()->
  lock.readLock()  //阻塞在悲观读锁
);
T2.start();
Thread.sleep(100);// 保证T2阻塞在读锁
T2.interrupt();//中断线程T2//会导致线程T2所在CPU飙升
T2.join();
2.4 标准StampedLock 读写模板

建议你在实际工作中尽量按照下面的模板来使用 StampedLock。

  • StampedLock 读模板:
final StampedLock sl =  new StampedLock();
// 乐观读
long stamp =  sl.tryOptimisticRead();
......// 读入方法局部变量
// 校验stamp
if (!sl.validate(stamp)){
  // 升级为悲观读锁
  stamp = sl.readLock();
  try {   
    ..... // 读入方法局部变量
  } finally {
    //释放悲观读锁
    sl.unlockRead(stamp);
  }
}
//使用方法局部变量执行业务操作
......
  • StampedLock 写模板:
long stamp = sl.writeLock();
try {
  ......// 写共享变量
} finally {
  sl.unlockWrite(stamp);
}

参考:极客时间
更多:邓新

发布了34 篇原创文章 · 获赞 0 · 访问量 1089

猜你喜欢

转载自blog.csdn.net/qq_42634696/article/details/105135368
今日推荐