Java并发编程札记-(四)JUC锁-07读写锁的升级—StampedLock

时间:2022-07-17 20:50:07

StampedLock是JDK1.8新增的一个锁,是对读写锁ReentrantReadWriteLock的改进。前面已经学习了ReentrantReadWriteLock,我们了解到,在共享数据很大,且读操作远多于写操作的情况下,ReentrantReadWriteLock值得一试。但要注意的是,只有当前没有线程持有读锁或者写锁时才能获取到写锁,这可能会导致写线程发生饥饿现象,即读线程太多导致写线程迟迟竞争不到锁而一直处于等待状态。StampedLock可以解决这个问题,解决方法是如果在读的过程中发生了写操作,应该重新读而不是直接阻塞写线程。

StampedLock有三种读/写模式:写、读、乐观读。

  • 写。独占锁,只有当前没有线程持有读锁或者写锁时才能获取到该锁。方法writeLock()返回一个可用于unlockWrite(long)释放锁的方法的戳记。tryWriteLock()提供不计时和定时的版本。
  • 读。共享锁,如果当前没有线程持有写锁即可获取该锁,可以由多个线程获取到该锁。方法readLock()返回可用于unlockRead(long)释放锁的方法的戳记。tryReadLock()也提供不计时和定时的版本。
  • 乐观读。方法tryOptimisticRead()仅当锁定当前未处于写入模式时,方法才会返回非零戳记。返回戳记后,需要调用validate(long stamp)方法验证戳记是否可用。也就是看当调用tryOptimisticRead返回戳记后到到当前时间是否有其他线程持有了写锁,如果有,返回false,否则返回true,这时就可以使用该锁了。

此类还支持有条件地提供三种模式转换的方法。例如,方法tryConvertToWriteLock(long)试图“升级”模式,如果(1)已经处于书写模式(2)处于阅读模式并且没有其他读取器或者(3)处于乐观模式且锁定可用,则返回有效的写入标记。这些方法的形式旨在帮助减少在基于重试的设计中发生的一些代码膨胀。

StampedLock不是可重入的。

下面是一个StampedLock注释中的例子。

class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
    /** * 改变当前坐标。 * 先获取写锁,然后对point坐标进行修改,最后释放锁。 * 该锁是排它锁,这保证了其他线程调用move函数时候会被阻塞,直到当前线程显示释放了该锁。 */
    void move(double deltaX, double deltaY) { // an exclusively locked method
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
    /** * 计算当前坐标到原点的距离 * * @return */
    double distanceFromOrigin() { // A read-only method
        //1.尝试获取乐观读锁,返回stamp
        long stamp = sl.tryOptimisticRead();
        //2.拷贝参数到本地方法栈中
        double currentX = x, currentY = y;
        //3.验证stamp是否有效
        if (!sl.validate(stamp)) {
            //4.如果stamp无效,说明得到stamp后,又有其他线程获得了写锁
            //5.获取读锁
            stamp = sl.readLock();
            try {
                //6.其他线程修改了x,y的值,为了数据的一致性,需要再次再次拷贝参数到本地方法栈中
                currentX = x;
                currentY = y;
            } finally {
                //7.释放读锁
                sl.unlockRead(stamp);
            }
        }
        //8.使用参数的拷贝来计算当前坐标到原点的距离。无论步骤3中stamp有没有验证成功,参数的拷贝都是当前坐标的值
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
    /** * 如果当前坐标为原点则移动到指定的位置 */
    void moveIfAtOrigin(double newX, double newY) { // upgrade
        // 获取读锁,保证其他线程不能获取到写锁
        long 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();//再获取写锁
                    //循环while中的操作,直到成功更新坐标值
                }
            }
        } finally {
            //最后释放写锁
            sl.unlock(stamp);
        }
    }
}

本文就讲到这里,想了解Java并发编程更多内容请参考: