Java并发StampedLock

228 阅读3分钟

文章目录

一.简介

读写锁(ReadWriteLock)允许多个线程同时读共享变量,适用于读多写少的场景,读多写少场景中比这更快的方案就是StampedLock,比读写锁性能好。

二.锁模式

ReadWriteLock支持两种模式:一种是读锁,一种是写锁,而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提供的乐观读,是允许一个线程获取写锁的,也就是锁不是所有的写操作都被阻塞。(乐观读这个操作是无锁的)

@Data
public class Point {
    private double x, y;//内部定义表示坐标点
    private final StampedLock s1 = new StampedLock();//定义了StampedLock锁,


    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }
    void move(double deltaX, double deltaY) {
        long stamp = s1.writeLock();//这里的含义和distanceFormOrigin方法中 s1.readLock()是类似的
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            s1.unlockWrite(stamp);//退出临界区,释放写锁
        }
    }
    double distanceFormOrigin() throws InterruptedException {//只读方法
        long stamp = s1.tryOptimisticRead();  //试图尝试一次乐观读 返回一个类似于时间戳的邮戳整数stamp 这个stamp就可以作为这一个所获取的凭证
        double currentX = x, currentY = y;//读取x和y的值,这时候我们并不确定x和y是否是一致的
        Thread.sleep(1000);
        if (!s1.validate(stamp)) {//判断这个stamp是否在读过程发生期间被修改过,如果stamp没有被修改过,责任无这次读取时有效的,因此就可以直接return了,反之,如果stamp是不可用的,则意味着在读取的过程中,可能被其他线程改写了数据,因此,有可能出现脏读,如果如果出现这种情况,我们可以像CAS操作那样在一个死循环中一直使用乐观锁,知道成功为止
            stamp = s1.readLock();//也可以升级锁的级别,这里我们升级乐观锁的级别,将乐观锁变为悲观锁, 如果当前对象正在被修改,则读锁的申请可能导致线程挂起.
            try {
                currentX = x;
                currentY = y;
                System.out.println("currentX :"+currentX);
                System.out.println("currentY :"+currentY);
            } finally {
                s1.unlockRead(stamp);//退出临界区,释放读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
    public static void main(String[] args) throws InterruptedException {
        Point point = new Point(10, 10);
        Thread t1 = new Thread(
                ()->{
                    try {
                        System.out.println("测试:" + point.distanceFormOrigin());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
        );        
        Thread t2 = new Thread(()->{
              point.move(20,20);
        }); 
        t1.start();
        t2.start();
        t2.join();   
    }
}

结果

currentX :30.0
currentY :30.0
测试:42.42640687119285

注意事项

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

  • StampedLock不支持重入
  • StampedLock 的悲观读锁、写锁都不支持条件变量,也就是没Condition。
  • 如果是线程使用writeLock()或者readLock()获得锁之后,线程还没执行完就被interrupt()的话,会导致CPU飙升。(如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly())。

三.总结

读模板

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

写模板

long stamp = sl.writeLock();
try {
// 写共享变量
......
} finally {
sl.unlockWrite(stamp);
}

参考

《Java并发编程实战》

公众号

在这里插入图片描述

微信号:bigdata_limeng