前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >阿里四轮面试遭遇StampedLock,这么应对保拿offer!

阿里四轮面试遭遇StampedLock,这么应对保拿offer!

作者头像
JavaEdge
发布2021-04-25 09:52:59
3380
发布2021-04-25 09:52:59
举报
文章被收录于专栏:JavaEdgeJavaEdge

读写锁允许多个线程同时读共享变量,适用于读多写少。 那在读多写少的场景中,有没有更快的技术方案呢?还真有,JDK在1.8提供StampedLock,其性能比读写锁还好。

StampedLock支持哪些锁模式?

我们知道ReadWriteLock支持读锁、写锁两种锁模式。而StampedLock支持三种:写锁、悲观读锁和乐观读。 其写锁、悲观读锁和ReadWriteLock的写锁、读锁的语义类似。不同在于:StampedLock的写锁和悲观读锁加锁成功后,都会返回一个stamp;释放锁时,需要传入该stamp。

那StampedLock的性能为啥比ReadWriteLock好呢?

核心在于StampedLock支持乐观读。ReadWriteLock支持多个线程同时读,但当多线程读时,所有的写操作会被阻塞;而StampedLock提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有写操作都被阻塞。

乐观读的操作是无锁的,所以相比较ReadWriteLock的读锁,乐观读性能更好。

那你工作中一般如何使用的呢?

该示例中,若执行乐观读过程中,存在写操作,会把乐观读升级为悲观读锁。这样很好,否则就要在一个循环里反复执行乐观读,直到执行乐观读操作的期间没有写操作(这样才能保证x和y的正确性和一致性),而循环读会浪费大量CPU。升级为悲观读锁,代码简练且不易出错。

谈谈你对乐观读的理解?

很多人喜欢类比StampedLock的乐观读和数据库的乐观锁。

数据库乐观锁使用场景是这样的:一个模块,会有多个人通过前端同时修改同一条订单,那如何保证订单数据是线程安全的呢? 这就可以使用乐观锁。

乐观锁实现很简单,在订单的表 product_doc 增加一个数值类型版本号字段 version,每次更新product_doc表时,将 version 字段加1。生产订单的UI在展示的时候,需要查询数据库,此时将这个 version 字段和其他业务字段一起返回给生产订单UI。 假设用户查询的生产订单的id=777,SQL如下:

代码语言:javascript
复制
select id,... ,version
from product_doc
where id=777

用户在前端执行保存操作的时候,后台利用下面的SQL语句更新生产订单,此处我们假设该条生产订单的 version=9。

代码语言:javascript
复制
update product_doc 
set version=version+1,...
where id=777 and version=9

如果这条SQL语句执行成功并且返回1,说明前端执行查询操作到执行保存操作期间,没有其他人修改过这条数据。因为如果这期间其他人修改过这条数据,那么版本号字段一定会大于9。、

数据库里的乐观锁,查询的时候需要把 version 字段查出来,更新的时候要利用 version 字段做验证。这个 version 字段就类似于StampedLock里面的stamp。

StampedLock使用时踩过什么坑吗?

读多写少场景StampedLock性能很好,可替代ReadWriteLock,但StampedLock不可重入。StampedLock的悲观读锁、写锁都不支持条件变量。

如果线程阻塞在StampedLock的readLock()或writeLock(),此时调用该阻塞线程的interrupt(),会导致CPU飙升。 例如下面的代码中,线程T1获取写锁之后将自己阻塞,线程T2尝试获取悲观读锁,也会阻塞;如果此时调用线程T2的interrupt()方法来中断线程T2的话,你会发现线程T2所在CPU会飙升到100%。

代码语言:javascript
复制
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();
// 保证T2阻塞在读锁
Thread.sleep(100);
//中断线程T2
//会导致线程T2所在CPU飙升
T2.interrupt();
T2.join();

所以使用StampedLock一定不要调用中断。如果需要支持中断功能,一定使用可中断的悲观读锁readLockInterruptibly()和写锁writeLockInterruptibly()。

总结

StampedLock的使用看上去有点复杂,但是如果你能理解乐观锁背后的原理,使用起来还是比较流畅的。建议你认真揣摩Java的官方示例,这个示例基本上就是一个最佳实践。我们把Java官方示例精简后,形成下面的代码模板,建议你在实际工作中尽量按照这个模板来使用StampedLock。

StampedLock读模板:

代码语言:javascript
复制
final StampedLock sl = 
  new StampedLock();

// 乐观读
long stamp = 
  sl.tryOptimisticRead();
// 读入方法局部变量
......
// 校验stamp
if (!sl.validate(stamp)){
  // 升级为悲观读锁
  stamp = sl.readLock();
  try {
    // 读入方法局部变量
    .....
  } finally {
    //释放悲观读锁
    sl.unlockRead(stamp);
  }
}
//使用方法局部变量执行业务操作

StampedLock写模板:

代码语言:javascript
复制
long stamp = sl.writeLock();
try {
  // 写共享变量
  ......
} finally {
  sl.unlockWrite(stamp);
}

StampedLock支持锁的降级(通过tryConvertToReadLock()方法实现)和升级(通过tryConvertToWriteLock()方法实现)。

代码语言:javascript
复制
private double x, y;
final StampedLock sl = new StampedLock();
// 存在问题的方法
void moveIfAtOrigin(double newX, double newY){
 long stamp = sl.readLock();
 try {
  while(x == 0.0 && y == 0.0){
    long ws = sl.tryConvertToWriteLock(stamp);
    if (ws != 0L) {
      x = newX;
      y = newY;
      break;
    } else {
      sl.unlockRead(stamp);
      stamp = sl.writeLock();
    }
  }
 } finally {
  sl.unlock(stamp);
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-04-23 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总结
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档