admin

JDK 并发编程 - 读写锁的改进:StampedLock
前面一片文章介绍过了ReadWriteLock读写锁的使用,虽然读写锁分离了读和写的功能,使得读与读之间可以完全并...
扫描右侧二维码阅读全文
28
2018/09

JDK 并发编程 - 读写锁的改进:StampedLock

前面一片文章介绍过了ReadWriteLock读写锁的使用,虽然读写锁分离了读和写的功能,使得读与读之间可以完全并发,但是读与写之间还是冲突的。
读锁会完全阻塞写锁,它使用的依然是悲观的锁策略,如果有大量的读线程,它就有可能引起写线程的饥饿(即长时间获取不到写锁,更新不了数据)
所以在JDK1.8中引入了一种新的锁机制:StampedLock。它提供了一种乐观的读策略,使得读线程可以完全不会阻塞写线程。
下面是 JDK 文档中的例子:

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    void move(double deltaX, double deltaY) { // 排它锁方法
        long 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 = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    void moveIfAtOrigin(double newX, double newY) { // 悲观读
        // 疑惑?一开始就使用悲观读锁,下面的tryConvertToWriteLock锁转换还会成功吗?
        // tryConvertToWriteLock 当参数 stamp 为读锁返回的时候,如果写锁可用,则将读锁升级为写锁并返回写锁的 stamp
        // readLock 一旦获取悲观读锁成功,就不可能有其他线程获取到写锁,那么写锁始终是不可用的 tryConvertToWriteLock 这个转换还会成功吗?
        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(); //再直接申请悲观写锁
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }
}

代码中的distanceFromOrigin方法使用的是乐观读,tryOptimisticRead会返回一个票据,validate方法根据传入的stamp判断在读过程中是否发生过修改,在上面代码中如果发现stamp被修改过,就使用readLock升级为悲观读,当然我们也可以使用类似CAS操作用一个死循环来一直乐观读,直到成功为止。

下面的moveIfAtOrigin悲观读方法,疑惑在注释中一直想不明白,知道的朋友帮忙解答下吧,查了很多文章都是说转换为写锁一笔带过,而没有去考虑整个方法到底起个什么作用,下面简单说下自己的理解:

既然需要修改数据,为什么不直接使用悲观写锁,而是一开始去获取一个悲观读锁?

因为由方法名moveIfAtOrigin可以知道,该方法是说如果当前点是原点的话就进行移动,也就是说当前点可能不是原点,那么如果我们直接使用悲观写锁的话,当前点不是原点,仅仅是为了读取这个点的位置就使用了悲观写锁,而写锁是会阻塞读锁的,所以造成浪费,尤其是当前点不在原点的概率比较大时,如果直接使用写锁的话性能会明显下降。而StampedLockReadWriteLock读写锁一样:读读之间是不会阻塞的,所以先使用读锁来判断当前点是否为原点。

当获取到悲观读锁,读到当前点是原点的时候,尝试将读锁转换为写锁,转换过程中之前的悲观读锁会不会释放?

tryConvertToWriteLock方法官方文档解释是,当传入的参数 stamp 是读锁的票据时,如果写锁可用,就释放读锁,返回写锁的 stamp 票据,注意:到底是先判断写锁是否可用,然后再释放读锁?还是先释放读锁,然后去竞争写锁?这点就是我没想明白的地方,如果是前者,读锁没释放之前,因为读锁和写锁之间是排斥的,下面的 Demo 程序可以验证(在读锁未释放之前,写锁是获取不到的),那么写锁肯定是不可用的,tryConvertToWriteLock必定返回 0 失败,然后执行代码中的

sl.unlockRead(stamp);   //转换失败,先释放读锁
stamp = sl.writeLock(); //再直接申请悲观写锁

那么上面官方例子中的tryConvertToWriteLock操作还有什么意义?不如直接去申请写锁。
后面一种情况就是tryConvertToWriteLock会先释放读锁然后去竞争写锁,因为是先释放的读锁,所以就有可能竞争成功。
读写锁验证 Demo

import java.util.Random;
import java.util.concurrent.locks.StampedLock;

public class StampedLockTest {

    static int x = 0;
    static StampedLock sl = new StampedLock();

    static int read() throws InterruptedException {
        long stamp = sl.readLock();
        try {
            System.out.println("x 开始读取");
            Thread.sleep(5000);
            System.out.println("x 读取完毕");
            return x;
        }  finally {
            sl.unlock(stamp);
        }
    }

    static void write(int n) {
        long stamp = sl.writeLock();
        try {
            x = n;
            System.out.println("x 被修改了");
        } finally {
            sl.unlock(stamp);
        }
    }

    public static void main(String[] args) {
        try {
            //读线程
            Thread t1 = new Thread(() -> {
                try {
                    read();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            //写线程
            Thread t2 = new Thread(() -> write(100));
            t1.start();
            Thread.sleep(1000);
            t2.start();
            t1.join();
            t2.join();
            System.out.println(x);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

运行上面的代码可以发现写操作总是在读操作之后执行,也就是说读写锁之间是互斥的,在申请到读锁的时候,在没有释放读锁之前,写锁始终是不可用的。

以上纯属个人理解,如果有误,还请赐教。

Last modification:September 28th, 2018 at 06:36 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment