Java多线程-StampedLock(原子读写锁)

StampedLock 是读写锁的实现,对比 ReentrantReadWriteLock 主要不同是该锁不允许重入,多了乐观读的功能,使用上会更加复杂一些,但是具有更好的性能表现。StampedLock 的状态由版本和读写锁持有计数组成。 获取锁方法返回一个邮戳,表示和控制与锁状态相关的访问; 这些方法的“尝试”版本可能会返回特殊值 0 来表示获取锁失败。 锁释放和转换方法需要邮戳作为参数,如果它们与锁的状态不匹配则失败。

但是也是由于 StampedLock 大量使用自旋的原因(ReentrantReadWriteLock 也使用了自旋,但是没有 StampedLock 频繁),CPU 的消耗理论上也比 ReentrantReadWriteLock 高。

StampedLock 非常适合写锁中的操作非常快的业务场景。因为读锁如果因为写锁而获取锁失败,读锁会做重试获取和有限次的自旋的方式,比较晚进入到等待队列中。如果在自旋过程中,写锁能释放,那么获取读锁的线程就能避免被操作系统阻塞和唤醒等耗资源操作,增加读锁的响应效率。

三种模式

悲观读锁

与 ReentrantReadWriteLock 的读锁类似,多个线程可以同时获取悲观读锁。这是一个共享锁,允许多个线程同时读取共享资源。

乐观读锁

相当于直接操作数据,不加任何锁。在操作数据前并没有通过 CAS 设置锁的状态,仅仅通过位运算测试。如果当前没有线程持有写锁,则简单地返回一个非 0 的 stamp 版本信息。返回 0 则说明有线程持有写锁。获取该 stamp 后在具体操作数据前还需要调用 validate 方法验证该 stamp 是否己经不可用。

写锁

与 ReentrantReadWriteLock的写锁类似,写锁和悲观读锁是互斥的。虽然写锁与乐观读锁不会互斥,但是在数据被更新之后,之前通过乐观读锁获得的数据已经变成了脏数据,需要自己处理这个。

StampedLock 的读写锁都是不可重入锁,所以在获取锁后释放锁前不应该再调用会获取锁的操作,以避免造成调用线程被阻塞。

在实际应用中,StampedLock 可以用于那些读操作远多于写操作的场景,例如缓存系统、数据报表生成等。在这些场景中,StampedLock 可以显著提高并发性能,同时保证数据的一致性和安全性。

最重要的一点: 在使用时需要特别注意:如果某个线程阻塞在StampedLock的readLock()或者writeLock()方法上时,此
时调用阻塞线程的interrupt()方法中断线程,会导致CPU飙升到100%。

所以尽量在写操作是非常快的场景下使用, 这样读的时候乐观锁释放的非常快,几乎达到无锁模式。

所有接口方法

image.png
image.png
image.png

经典案例

import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
    private int inventory = 100; // 初始库存为100
    private final StampedLock lock = new StampedLock();

    // 扣减库存操作
    public void decreaseInventory(int quantity) {
        long stamp = lock.writeLock(); // 获取写锁
        try {
            if (inventory >= quantity) {
                inventory -= quantity; // 扣减库存
                System.out.println("成功减少库存 " + quantity + ", 当前的库存量: " + inventory);
            } else {
                System.out.println("未能减少库存,库存不足");
            }
        } finally {
            lock.unlockWrite(stamp); // 释放写锁
        }
    }

    // 获取当前库存
    public int getInventory() {
        long stamp = lock.tryOptimisticRead(); // 乐观读锁
        int currentInventory = inventory;
        if (!lock.validate(stamp)) { // 检查乐观读锁是否有效
            stamp = lock.readLock(); // 乐观读锁无效,转为悲观读锁
            try {
                currentInventory = inventory; // 获取当前库存
            } finally {
                lock.unlockRead(stamp); // 释放读锁
            }
        }
        return currentInventory; // 返回当前库存
    }

    public static void main(String[] args) {
        StampedLockExample manager = new StampedLockExample();
        // 多个线程同时扣减库存
        Thread t1 = new Thread(() -> {
            manager.decreaseInventory(20); // 线程1扣减库存
            System.out.println(manager.getInventory());
        });
        Thread t2 = new Thread(() -> {
            manager.decreaseInventory(50); // 线程2扣减库存
            System.out.println(manager.getInventory());
        });
        t1.start();
        t2.start();
    }
}

官网案例

 
public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
 
    public void move(double deltaX, double deltaY) {
        使用写锁-独占操作,并返回一个邮票
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            使用邮票来释放写锁
            sl.unlockWrite(stamp);      
        }
    }
 
    
    // 使用乐观读锁访问共享资源
    // 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其 
    // 他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是 
    // 最新的数据,但是一致性还是得到保障的。
    public 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);
    }
 
 
    // 锁升级:读锁--> 写锁
    public void moveIfAtOrigin(double newX, double newY) { // upgrade
        // Could instead start with optimistic, not read mode
        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);
        }
    }
 
 
}

StampedLock和ReentrantReadWriteLock之间的区别

  1. 锁的类型与特性
    • StampedLock:提供了乐观读、悲观读和写锁三种模式。乐观读模式允许在写锁未被持有时进行无锁读取,通过验证戳记(stamp)来确保数据的一致性。这种模式减少了锁的竞争,提高了吞吐量。
    • ReentrantReadWriteLock:允许多个读线程同时访问,但写线程在访问时必须独占。它支持锁的重入,即同一线程可以多次获取同一把锁。
  2. 性能
    • StampedLock:通常比ReentrantReadWriteLock具有更高的性能,特别是在读多写少的场景下。由于乐观读的存在,它能够在无竞争的情况下避免不必要的锁开销。
    • ReentrantReadWriteLock:在读操作远多于写操作的场景中表现良好,但写锁的饥饿问题和锁降级操作可能影响其性能。
  3. 实现机制
    • StampedLock:并非基于AQS(AbstractQueuedSynchronizer)实现,而是使用了自己的同步等待队列和状态设计。其状态为一个long型变量,与ReentrantReadWriteLock的设计不同。
    • ReentrantReadWriteLock:基于AQS实现,通过内部维护的读写锁来实现多线程间的同步。
  4. 使用场景
    • StampedLock:更适合于读多写少且对性能要求较高的场景,尤其是当数据争用不严重时。它能够有效减少锁的竞争,提高系统的吞吐量。
    • ReentrantReadWriteLock:适用于需要重入锁或需要在写操作后降级为读锁的场景。它提供了更严格的访问控制,但可能在某些情况下牺牲了一定的性能。
  5. 锁的获取与释放
    • StampedLock:在获取锁时会返回一个戳记(stamp),用于后续的锁释放或转换。这个戳记代表了锁的状态,有助于在释放锁时验证数据的一致性。
    • ReentrantReadWriteLock:没有戳记的概念,锁的获取和释放相对简单直接。

综上所述,StampedLock和ReentrantReadWriteLock各有其特点和适用场景。在选择使用哪种锁时,应根据具体的应用需求和性能要求来做出决策。

点赞 -收藏 -关注
有问题在评论区或者私信我-收到会在第一时间回复
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

胡安民

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

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

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

打赏作者

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

抵扣说明:

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

余额充值