StampedLock(标记锁)
StampedLock的定义
StampedLock是Java 8中引入的一种乐观锁实现方式,它是通过使用标记(stamp)来协调多个线程对共享资源的访问。StampedLock提供了三种锁模式:读锁、写锁和乐观读锁,可以更加灵活地控制对共享资源的访问。
与传统的悲观锁相比,乐观锁通常更适用于读多写少的场景,并且可以提高并发访问效率。在StampedLock中,乐观读锁的获取和释放是非常快速的,因为它不会阻塞其他线程。但是,如果在读操作期间有其他线程修改了共享资源,则需要重新获取悲观读锁或写锁,以确保数据的一致性。
需要注意的是,StampedLock的锁是无法重入的,因此需要格外小心使用。另外,StampedLock还提供了类似于ReentrantReadWriteLock的功能,可以同时支持读写锁,但相比之下,StampedLock的实现更加轻量级,因此在某些场景下可以提供更好的性能表现。
乐观锁与悲观锁
悲观锁是一种较为保守的锁机制,它假定并发访问的线程会导致竞争和冲突,因此每个线程在访问共享资源时都会获取锁并阻塞其他线程的访问,直到自己完成操作并释放锁,其他线程才能再次获取锁并访问共享资源。悲观锁的代表是synchronized和ReentrantLock等锁。
乐观锁则是一种更加乐观的锁机制,它假定并发访问的线程不会发生冲突,因此每个线程在访问共享资源时不会阻塞其他线程的访问,而是直接执行操作。在执行操作之前,乐观锁会先尝试获取锁,并检查共享资源是否被修改,如果未被修改,则操作成功并返回结果;如果被修改了,则需要重新获取锁并重试。乐观锁的代表是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
可以看到读锁未释放就获取到写锁了,这正是乐观锁的作用。 最后得出的结果是修改之后的结果,证明修改在乐观读锁验证前修改完成。