【多线程】Java多线程基础(10)- StampedLock的使用

160 阅读4分钟

StampedLock(标记锁)

StampedLock的定义

StampedLock是Java 8中引入的一种乐观锁实现方式,它是通过使用标记(stamp)来协调多个线程对共享资源的访问。StampedLock提供了三种锁模式:读锁、写锁和乐观读锁,可以更加灵活地控制对共享资源的访问。

与传统的悲观锁相比,乐观锁通常更适用于读多写少的场景,并且可以提高并发访问效率。在StampedLock中,乐观读锁的获取和释放是非常快速的,因为它不会阻塞其他线程。但是,如果在读操作期间有其他线程修改了共享资源,则需要重新获取悲观读锁或写锁,以确保数据的一致性。

需要注意的是,StampedLock的锁是无法重入的,因此需要格外小心使用。另外,StampedLock还提供了类似于ReentrantReadWriteLock的功能,可以同时支持读写锁,但相比之下,StampedLock的实现更加轻量级,因此在某些场景下可以提供更好的性能表现。

乐观锁与悲观锁

悲观锁是一种较为保守的锁机制,它假定并发访问的线程会导致竞争和冲突,因此每个线程在访问共享资源时都会获取锁并阻塞其他线程的访问,直到自己完成操作并释放锁,其他线程才能再次获取锁并访问共享资源。悲观锁的代表是synchronizedReentrantLock等锁。

乐观锁则是一种更加乐观的锁机制,它假定并发访问的线程不会发生冲突,因此每个线程在访问共享资源时不会阻塞其他线程的访问,而是直接执行操作。在执行操作之前,乐观锁会先尝试获取锁,并检查共享资源是否被修改,如果未被修改,则操作成功并返回结果;如果被修改了,则需要重新获取锁并重试。乐观锁的代表是Atomic类和StampedLock等锁。

悲观锁和乐观锁各有优缺点,悲观锁可以保证数据的一致性和安全性,但会导致线程阻塞和性能下降乐观锁可以提高并发访问效率,但需要保证数据的一致性,并且在并发访问较高的情况下容易发生竞争和冲突,需要进行重试和处理。

因此,在使用锁机制时,需要根据具体的场景和需求进行选择,悲观锁适用于写多读少的场景,而乐观锁适用于读多写少的场景

使用StampedLock的案例

这里主要使用StampedLock的乐观读锁。

import java.util.concurrent.locks.StampedLock;

public class Main {
    public static void main(String[] args) {
        Point point = new Point();
        Thread thread1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                double v = point.distanceFromOrigin();
                System.out.println("thread1结果:"+ v);
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                point.move(3,4);
            }
        });
        thread1.start();
        thread2.start();
    }
}


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

    // 移动x,y
    public void move(double deltaX, double deltaY) {
        // 获取写锁
        long stamp = lock.writeLock();
        System.out.println(Thread.currentThread().getName() + "获得写锁");
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            // 释放写锁
            lock.unlockWrite(stamp);
        }
    }

    // 计算与原点的距离
    public double distanceFromOrigin() throws InterruptedException {
        // 获取乐观读锁, 返回一个版本号
        long stamp = lock.tryOptimisticRead();
        System.out.println(Thread.currentThread().getName() + "获得读锁");
        double currentX = x, currentY = y;
        // 验证版本号是否有改变
        // 如果在这时值被修改,就会重新获取读锁。
        if (!lock.validate(stamp)) {
            stamp = lock.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                lock.unlockRead(stamp);
                System.out.println("读锁释放");
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

我们首先使用tryOptimisticRead()方法获取乐观读锁的标记(stamp),并读取共享资源的值,然后使用validate()方法验证锁状态是否被修改。如果锁状态没有被修改,则直接返回读取到的值;如果锁状态被修改了,则需要重新获取悲观读锁,以确保读取到的值是最新的。

如果在使用StampedLock的乐观锁时,需要对共享资源进行比较复杂的操作,可以考虑使用StampedLock的带有版本号的方法来实现。在调用带有版本号的方法时,StampedLock会返回一个版本号,用于标识当前共享资源的版本。如果在执行操作的过程中共享资源的版本发生了变化,则StampedLock的方法会返回0,表示操作失败,此时需要重新执行操作。如果版本号没有发生变化,则操作成功并返回非0值。

输出分析

输出:
Thread-0获得读锁
Thread-1获得写锁
读锁释放
thread1结果:5.0

可以看到读锁未释放就获取到写锁了,这正是乐观锁的作用。 最后得出的结果是修改之后的结果,证明修改在乐观读锁验证前修改完成。