并发编程原理与实战(二十三)StampedLock应用实战与各种锁性能对比分析

上一篇我们学习了StampedLock邮戳锁引入背景以及邮戳锁的三种锁模式、邮戳锁的特性以及邮戳锁的核心api。本来就来进一步学习邮戳锁的具体使用。

首先我们先来分析官方提供的实例,分析StampedLock的主要api方法的使用方法。官方提供的例子是多线程环境下更新点的坐标并计算到原点的距离,这个功能在日常的业务开发中并不是很常见,但不妨碍我们对相关api使用的了解。

多线程更新坐标并计算到原点的距离

核心需求分析

1、线程安全的更新坐标值

  • 写操作需独占锁保证原子性,防止并发修改导致数据错乱
  • 使用writeLock()获取写锁,更新后通过unlockWrite(stamp)释放写锁。

2、高效的距离计算

  • 优先采用乐观读(tryOptimisticRead)减少锁竞争,以便适合高频读取的场景。
  • 若检测到写冲突(validate(stamp)失败),降级为悲观读锁重新获取数据。

3、数据一致性保证

  • 乐观读需拷贝共享变量到局部变量,避免验证后数据被修改。
  • 悲观读锁与写锁互斥,确保强一致性但可能降低吞吐量。

代码实现

我们对官方提供的例子稍微做了改造和备注,如下:

public class Point {
    // 坐标点
    private static double x, y;
    // 创建邮戳锁
    private static final StampedLock sl = new StampedLock();

    // 一个独占锁方法(写锁是独占的)
    public static void move(double deltaX, double deltaY) {
        // 获取写锁
        long stamp = sl.writeLock();
        try {
            // 修改坐标
            x += deltaX;
            y += deltaY;
            System.out.printf("Thread %s moved to (%.1f, %.1f)%n", Thread.currentThread().getName(), x, y);
        } finally {
            // 根据邮戳释放写锁
            sl.unlockWrite(stamp);
        }
    }

    // 一个只读的方法
    // 从乐观读锁升级为读锁(悲观读)
    public static double distanceFromOrigin() {
        // 尝试乐观读锁,返回非0的stamp表示当前无写锁
        long stamp = sl.tryOptimisticRead();
        try {
            retryHoldingLock:
            // 获取共享读锁
            for (; ; stamp = sl.readLock()) {
                // 存在写锁则重试获取写锁
                if (stamp == 0L)
                    continue retryHoldingLock;
                // 尽可能单纯的读操作
                double currentX = x;
                double currentY = y;
                //检查乐观读期间是否有写操作,返回true表示数据有效,若返回false,需升级为悲观读或重试
                if (!sl.validate(stamp))
                    continue retryHoldingLock;
                //计算二维平面上点 (x, y) 到原点 (0, 0)
                double distance = Math.hypot(currentX, currentY);
                System.out.printf("Thread %s calculated distance: %.2f%n", Thread.currentThread().getName(), distance);
                return distance;
            }
        } finally {
            //判断邮戳代表的是否是读锁
            //if (StampedLock.isReadLockStamp(stamp))
            if (isReadLockStamp(stamp))
                //释放读锁
                sl.unlockRead(stamp);
        }
    }

    // 乐观读锁升级为写锁
    public static void moveIfAtOrigin(double newX, double newY) {
        long stamp = sl.tryOptimisticRead();
        try {
            retryHoldingLock:
            for (; ; stamp = sl.writeLock()) {
                if (stamp == 0L)
                    continue retryHoldingLock;
                double currentX = x;
                double currentY = y;
                if (!sl.validate(stamp))
                    continue retryHoldingLock;
                if (currentX != 0.0 || currentY != 0.0)
                    break;
                //尝试将读锁升级为写锁,返回新stamp或0。
                stamp = sl.tryConvertToWriteLock(stamp);
                if (stamp == 0L)
                    //升级失败重新获取写锁。
                    continue retryHoldingLock;
                // 排他访问
                x = newX;
                y = newY;
                return;
            }
        } finally {
            //判断邮戳代表的是否是写锁
            //if (StampedLock.isWriteLockStamp(stamp))
            if (!isReadLockStamp(stamp))
                //释放写锁
                sl.unlockWrite(stamp);
        }
    }

    // 读锁升级为写锁
    public static void moveIfAtOrigin2(double newX, double newY) {
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                //尝试将读锁升级为写锁,返回新stamp或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);
        }
    }

    /**判断是否是读锁**/
    public static boolean isReadLockStamp(long stamp) {
        return (stamp & 0xFFL) != 0L;
    }
}

读写线程比例设计

例子中并没有提供怎么使用多线程来读写坐标的方法,所以我们来补充这个多线程读写的方法。既然StampedLock适合在读多写少的场景中使用,那么读线程要比写线程多,以便模拟高频读和低频写操作的场景,且写线程间隔时间应明显大于读线程(如5秒 vs 300ms)以观察锁竞争情况。基于上述分析,我们增加一个main函数来测试下上述代码。

public static void main(String[] args) {
    //创建1个写线程
    new Thread(() -> {
        for (; ; ) {
            move(2.0, 2.0);
            try {
                //5秒写一次坐标
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();

    //创建9个读线程
    for (int i = 0; i < 9; i++) {
        new Thread(() -> {
            for (; ; ) {
                distanceFromOrigin();
                try {
                    //300毫秒秒读一次坐标
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

main函数中,我们创建一个线程不断的移动坐标,每5秒执行一次移动坐标操作(写操作);创建9个读线程不断读坐标值并计算到圆点的距离,每300毫秒秒读一次坐标。观察下运行结果:

Thread Thread-0 moved to (2.0, 2.0)
Thread Thread-2 calculated distance: 2.83
Thread Thread-4 calculated distance: 2.83
Thread Thread-5 calculated distance: 2.83
Thread Thread-7 calculated distance: 2.83
Thread Thread-6 calculated distance: 2.83
Thread Thread-8 calculated distance: 2.83
Thread Thread-1 calculated distance: 2.83
Thread Thread-3 calculated distance: 2.83
Thread Thread-9 calculated distance: 2.83
...
Thread Thread-0 moved to (4.0, 4.0)
Thread Thread-8 calculated distance: 5.66
Thread Thread-7 calculated distance: 5.66
Thread Thread-5 calculated distance: 5.66
Thread Thread-3 calculated distance: 5.66
Thread Thread-1 calculated distance: 5.66
Thread Thread-2 calculated distance: 5.66
Thread Thread-4 calculated distance: 5.66
Thread Thread-9 calculated distance: 5.66
...

从运行结果可以看出,写操作和读操作是互斥的,数据没有任何错乱。

各种锁性能对比分析

乐观读加锁

那问题来了,这个例子中怎么体现使用StampedLock的乐观读后带来的性能提升?所以我们得用之前已经掌握的锁来读操作来进行对比。首先先对distanceFromOrigin()方法进行改造,增加读取耗时打印,如下:

// 一个只读的方法
// 从乐观读锁升级为读锁(悲观读)
public static double distanceFromOrigin() {
    long startTime = System.currentTimeMillis();
    // 尝试乐观读锁,返回非0的stamp表示当前无写锁
    long stamp = sl.tryOptimisticRead();
    double distance = 0;
    try {
        retryHoldingLock:
        // 获取共享读锁
        for (; ; stamp = sl.readLock()) {
            // 存在写锁则重试获取写锁
            if (stamp == 0L)
                continue retryHoldingLock;
            // 尽可能单纯的读操作
            double currentX = x;
            double currentY = y;
            //检查乐观读期间是否有写操作,返回true表示数据有效,若返回false,需升级为悲观读或重试
            if (!sl.validate(stamp))
                continue retryHoldingLock;
            //计算二维平面上点 (x, y) 到原点 (0, 0)
            distance = Math.hypot(currentX, currentY);
            System.out.printf("Thread %s calculated distance: %.2f%n", Thread.currentThread().getName(), distance);
            break;
        }
    } finally {
        //判断邮戳代表的是否是读锁
        //if (StampedLock.isReadLockStamp(stamp))
        if (isReadLockStamp(stamp))
            //释放读锁
            sl.unlockRead(stamp);

        long endTime = System.currentTimeMillis();
        System.out.println("Thread "+Thread.currentThread().getName()+" calculated distance cost time: "+(endTime-startTime));
        return distance;
    }
}

运行结果如下:

Thread Thread-1 calculated distance: 0.00
Thread Thread-0 moved to (2.0, 2.0)
Thread Thread-1 calculated distance cost time: 37
Thread Thread-2 calculated distance: 2.83
Thread Thread-2 calculated distance cost time: 29
Thread Thread-9 calculated distance: 2.83
Thread Thread-8 calculated distance: 2.83
Thread Thread-8 calculated distance cost time: 26
Thread Thread-7 calculated distance: 2.83
Thread Thread-7 calculated distance cost time: 26
Thread Thread-6 calculated distance: 2.83
Thread Thread-6 calculated distance cost time: 27
Thread Thread-5 calculated distance: 2.83
Thread Thread-5 calculated distance cost time: 27
Thread Thread-4 calculated distance: 2.83
Thread Thread-4 calculated distance cost time: 30
Thread Thread-3 calculated distance: 2.83
Thread Thread-3 calculated distance cost time: 31
Thread Thread-9 calculated distance cost time: 25

synchronized加锁

下面使用synchronized对读取坐标过程加锁,代码如下:

public static double distanceFromOrigin2() {
    long startTime = System.currentTimeMillis();
    double distance = 0;
    synchronized (Point.class) {
        // 尽可能单纯的读操作
        double currentX = x;
        double currentY = y;
        distance = Math.hypot(currentX, currentY);
        System.out.printf("Thread %s calculated distance: %.2f%n", Thread.currentThread().getName(), distance);
    }
    long endTime = System.currentTimeMillis();
    System.out.println("Thread " + Thread.currentThread().getName() + " calculated distance cost time: " + (endTime - startTime));
    return distance;
}

main方法中读线程改成调用该方法

//创建10个读线程
for (int i = 0; i < 9; i++) {
    new Thread(() -> {
        for (; ; ) {
            //distanceFromOrigin();
            distanceFromOrigin2();
            try {
                //300毫秒秒写一次坐标
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

看看运行结果

Thread Thread-0 moved to (2.0, 2.0)
Thread Thread-2 calculated distance: 2.83
Thread Thread-2 calculated distance cost time: 31
Thread Thread-9 calculated distance: 2.83
Thread Thread-9 calculated distance cost time: 8
Thread Thread-8 calculated distance: 2.83
Thread Thread-8 calculated distance cost time: 9
Thread Thread-7 calculated distance: 2.83
Thread Thread-7 calculated distance cost time: 14
Thread Thread-6 calculated distance: 2.83
Thread Thread-6 calculated distance cost time: 31
Thread Thread-5 calculated distance: 2.83
Thread Thread-5 calculated distance cost time: 32
Thread Thread-4 calculated distance: 2.83
Thread Thread-4 calculated distance cost time: 33
Thread Thread-3 calculated distance: 2.83
Thread Thread-3 calculated distance cost time: 34
Thread Thread-1 calculated distance: 2.83
Thread Thread-1 calculated distance cost time: 34

从运行结果看,synchronized的性能视乎更好,可能因为synchronized内部已经做了优化的结果。

ReentrantLock加锁

我们换成ReentrantLock看看。

// 一个只读的方法
// ReentrantLock实现
public static double distanceFromOrigin3() {
    long startTime = System.currentTimeMillis();
    double distance = 0;
    lockObject.lock();
    try {
        // 尽可能单纯的读操作
        double currentX = x;
        double currentY = y;
        distance = Math.hypot(currentX, currentY);
    } finally {
        lockObject.unlock();
    }
    System.out.printf("Thread %s calculated distance: %.2f%n", Thread.currentThread().getName(), distance);
    long endTime = System.currentTimeMillis();
    System.out.println("Thread " + Thread.currentThread().getName() + " calculated distance cost time: " + (endTime - startTime));
    return distance;
}

运行结果

Thread Thread-0 moved to (2.0, 2.0)
Thread Thread-1 calculated distance: 2.83
Thread Thread-2 calculated distance: 2.83
Thread Thread-2 calculated distance cost time: 38
Thread Thread-3 calculated distance: 2.83
Thread Thread-3 calculated distance cost time: 34
Thread Thread-4 calculated distance: 2.83
Thread Thread-4 calculated distance cost time: 35
Thread Thread-5 calculated distance: 2.83
Thread Thread-5 calculated distance cost time: 35
Thread Thread-6 calculated distance: 2.83
Thread Thread-6 calculated distance cost time: 31
Thread Thread-7 calculated distance: 2.83
Thread Thread-7 calculated distance cost time: 32
Thread Thread-8 calculated distance: 2.83
Thread Thread-8 calculated distance cost time: 31
Thread Thread-9 calculated distance: 2.83
Thread Thread-9 calculated distance cost time: 31
Thread Thread-1 calculated distance cost time: 39

换成ReentrantLock后,从运行结果可以看出,耗时明细长了,都在30毫秒以上,这从侧面印证乐观锁的性能确实比ReentrantLock好。

ReentrantReadWriteLock加锁

最后用ReentrantReadWriteLock对读取坐标过程加锁。

private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

// 一个只读的方法
// ReentrantReadWriteLock实现
public static double distanceFromOrigin4() {
    long startTime = System.currentTimeMillis();
    double distance = 0;
    reentrantReadWriteLock.readLock().lock();
    try {
        // 尽可能单纯的读操作
        double currentX = x;
        double currentY = y;
        distance = Math.hypot(currentX, currentY);
    } finally {
        reentrantReadWriteLock.readLock().unlock();
    }
    System.out.printf("Thread %s calculated distance: %.2f%n", Thread.currentThread().getName(), distance);
    long endTime = System.currentTimeMillis();
    System.out.println("Thread " + Thread.currentThread().getName() + " calculated distance cost time: " + (endTime - startTime));
    return distance;
}

main方法中读线程改成调用该方法,运行结果如下:

Thread Thread-0 moved to (2.0, 2.0)
Thread Thread-7 calculated distance: 2.83
Thread Thread-8 calculated distance: 2.83
Thread Thread-8 calculated distance cost time: 64
Thread Thread-6 calculated distance: 2.83
Thread Thread-6 calculated distance cost time: 64
Thread Thread-9 calculated distance: 2.83
Thread Thread-9 calculated distance cost time: 63
Thread Thread-2 calculated distance: 2.83
Thread Thread-2 calculated distance cost time: 65
Thread Thread-5 calculated distance: 2.83
Thread Thread-5 calculated distance cost time: 64
Thread Thread-4 calculated distance: 2.83
Thread Thread-4 calculated distance cost time: 65
Thread Thread-3 calculated distance: 2.83
Thread Thread-3 calculated distance cost time: 66
Thread Thread-1 calculated distance: 2.83
Thread Thread-1 calculated distance cost time: 66
Thread Thread-7 calculated distance cost time: 63
...

从运行结果来看,ReentrantReadWriteLock加锁后耗时最久。

四种锁性能对比

对上面的四种锁对读取坐标过程加锁后的读取耗时的前9次,即每个线程的首次读取耗时,我们取平均值来对比四种锁的性能:

StampedLock乐观锁 :平均耗时28.66毫秒
synchronized :平均耗时25.11毫秒
ReentrantLock :平均耗时34毫秒
ReentrantReadWriteLock:平均耗时64毫秒

从对比来看synchronized的性能最好,其次到StampedLock乐观锁,ReentrantLock表现一般ReentrantReadWriteLock的性能最差。这从侧面说明了StampedLock的乐观读确实比读写锁带来了性能提升。

总结

本文通过官方提供的多线程更新坐标并计算到原点的距离这个例子,说明了StampedLock的应用,然后通过设计不同的读写线程,分析读写坐标时的数据一致性,最后通过用ReentrantLock和synchronized分别对读取坐标进行加锁,对比了与StampedLock 乐观锁实现的耗时。通过该例子,进一步加深了对StampedLock 的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

帧栈

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值