seo排名优化价格,aso优化方法,怎么把网站维护,深圳排名优化哪家专业文章目录 一、理解“戳” (Stamp)二、为什么 StampedLock 能提高读性能#xff1f;秘密在于“乐观读”StampedLock性能对比性能对比结果图 总结 StampedLock完整演示代码对代码的疑问之处问题一#xff1a;为什么 demonstrateOptimisticReadFailure 中写线程能修改成功#… 文章目录 一、理解“戳” (Stamp)二、为什么 StampedLock 能提高读性能秘密在于“乐观读”StampedLock性能对比性能对比结果图 总结 StampedLock完整演示代码对代码的疑问之处问题一为什么 demonstrateOptimisticReadFailure 中写线程能修改成功问题二锁升级这块不是很理解为什么结果是成功的1. 什么是锁升级为什么需要它2. 锁升级什么时候会成功什么时候会失败3. 为什么您的代码里升级成功了4. finally 块中的 lock.unlock(stamp) 总结 直击了 StampedLock 的设计核心。解释它提升读性能的秘密。 一、理解“戳” (Stamp)
首先我们来理解“戳”Stamp是什么。
在 ReentrantReadWriteLock 中lock() 和 unlock() 是没有参数和返回值的。你只要调用 lock() 获取锁用完后调用 unlock() 释放锁即可。
StampedLock 完全不同。它的所有“上锁”操作都会返回一个long类型的数字这个数字就是所谓的**“戳” (Stamp)。而它所有的“解锁”操作都必须传入这个“戳”**。
long stamp lock.writeLock(); // 上写锁返回一个戳
try {// ...
} finally {lock.unlockWrite(stamp); // 解锁时必须传入获取锁时得到的那个戳
}为什么需要“戳” 这个“戳”本质上是一个版本号或者状态快照。
当没有任何锁时它是一个初始值。当有线程获取写锁时这个“戳”的值会发生改变比如增加一个版本号。每次上锁操作返回的“戳”都是独一无二的。
所以这句话 “在使用读锁、写锁时都必须配合【戳】使用”指的就是这种 lock() 返回戳、unlock() 传入戳的使用模式。这个“戳”是锁状态的凭证。
特别强调获取写锁的时候版本号才会改变如果我们获取读锁是不会修改版本号的 二、为什么 StampedLock 能提高读性能秘密在于“乐观读”
ReentrantReadWriteLock 无论如何都是一种悲观锁。即使是读锁当多个读线程和写线程竞争时仍然需要排队、阻塞、上下文切换这些都有性能开销。它总是悲观地认为“我读的时候很可能会有别人来写”。
StampedLock 之所以性能更高是因为它引入了一种全新的、ReentrantReadWriteLock 没有的模式——乐观读 (Optimistic Reading)。
乐观读的核心思想是我非常乐观地认为在我读取共享数据期间根本不会有线程来修改它。
基于这个乐观的假设StampedLock 的乐观读操作如下 尝试乐观读 (tryOptimisticRead): 线程想读取共享数据它先调用 lock.tryOptimisticRead()。这个方法不会加任何锁不会阻塞线程它只是瞬间获取一下当前的“戳”版本号然后立即返回。这个过程几乎没有开销速度极快。 读取共享数据: 线程拿着这个“戳”然后去读取共享变量比如 x, y 的值。 验证“戳” (validate): 读完数据后线程必须调用 lock.validate(stamp)并传入第一步获取的那个“戳”。validate 方法会检查从第一步到当前时刻有没有写操作发生过。它的判断依据就是**“戳”的版本号有没有变**。 如果版本号没变 (validate返回true)这说明在刚才的读取期间没有任何写操作来干扰。那么我们刚才读取的数据就是一致的、有效的。这次“乐观读”成功了如果版本号变了 (validate返回false)这说明在我们读取数据的过程中有一个“写线程”插了进来获取了写锁并修改了数据。那么我们刚才读到的数据就是“脏”的、不可信的。 失败后的补偿: 如果 validate 失败了说明乐观失败了我们不能再这么乐观。此时程序必须“升级”为悲观的读锁即调用 lock.readLock() 来老老实实地加锁然后重新读取一遍数据。
性能提升的关键点 在“读多写少”的场景下绝大多数的乐观读操作都会成功。成功的乐观读其开销仅仅是两次方法调用和一次版本号比较完全没有线程阻塞和上下文切换的开销甚至没有CAS操作的开销性能几乎和无锁操作一样快。
只有在极少数情况下读的过程中发生了写乐观读才会失败并升级为悲观读锁付出一点额外代价。但总体算下来性能提升是巨大的。 StampedLock性能对比
package StampLock;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.StampedLock;public class StampedLockPerformanceDemo {// 共享数据static class SharedData {private int value 0;// 三种不同的锁private final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock();private final StampedLock stampedLock new StampedLock();// 1. 使用 ReentrantReadWriteLock 读取public int readWithRWLock() {rwLock.readLock().lock();try {return value;} finally {rwLock.readLock().unlock();}}// 2. 使用 StampedLock 的悲观读public int readWithStampedLock() {long stamp stampedLock.readLock();try {return value;} finally {stampedLock.unlockRead(stamp);}}// 3. 使用 StampedLock 的乐观读性能最好public int readWithOptimisticRead() {// 获取乐观读戳long stamp stampedLock.tryOptimisticRead();// 读取数据无锁int currentValue value;// 验证期间是否有写操作if (!stampedLock.validate(stamp)) {// 升级为悲观读stamp stampedLock.readLock();try {currentValue value;} finally {stampedLock.unlockRead(stamp);}}return currentValue;}// 写操作使用 StampedLockpublic void write(int newValue) {long stamp stampedLock.writeLock();try {value newValue;} finally {stampedLock.unlockWrite(stamp);}}}public static void main(String[] args) throws InterruptedException {SharedData data new SharedData();int threadCount 100;int iterations 100000;// 测试不同读锁的性能System.out.println(开始性能测试...\n);// 1. 测试 ReentrantReadWriteLocklong startTime System.currentTimeMillis();testReadPerformance(data, threadCount, iterations, RWLock);long rwLockTime System.currentTimeMillis() - startTime;System.out.println(ReentrantReadWriteLock 耗时: rwLockTime ms);// 2. 测试 StampedLock 悲观读startTime System.currentTimeMillis();testReadPerformance(data, threadCount, iterations, StampedLock);long stampedLockTime System.currentTimeMillis() - startTime;System.out.println(StampedLock 悲观读 耗时: stampedLockTime ms);// 3. 测试 StampedLock 乐观读startTime System.currentTimeMillis();testReadPerformance(data, threadCount, iterations, OptimisticRead);long optimisticTime System.currentTimeMillis() - startTime;System.out.println(StampedLock 乐观读 耗时: optimisticTime ms);System.out.println(\n性能提升: String.format(%.2f, (double)rwLockTime / optimisticTime) 倍);}private static void testReadPerformance(SharedData data, int threadCount,int iterations, String lockType)throws InterruptedException {CountDownLatch latch new CountDownLatch(threadCount);for (int i 0; i threadCount; i) {new Thread(() - {for (int j 0; j iterations; j) {switch (lockType) {case RWLock:data.readWithRWLock();break;case StampedLock:data.readWithStampedLock();break;case OptimisticRead:data.readWithOptimisticRead();break;}}latch.countDown();}).start();}latch.await();}
}无锁的乐观读对性能提升极其明显提升足足150倍但是呢StampedLock也不是万能的他只是首先尝试使用乐观读无锁如果发现版本号不对劲中间被修改过了就需要加锁重新读取丢掉脏数据 关键代码
// 3. 使用 StampedLock 的乐观读性能最好public int readWithOptimisticRead() {// 获取乐观读戳long stamp stampedLock.tryOptimisticRead();// 读取数据无锁int currentValue value;// 验证期间是否有写操作if (!stampedLock.validate(stamp)) {// 升级为悲观读stamp stampedLock.readLock();try {currentValue value;} finally {stampedLock.unlockRead(stamp);}}return currentValue;}性能对比结果图 总结
“配合【戳】使用”指的是StampedLock所有上锁/解锁操作都围绕一个long类型的版本号戳来进行。提高读性能的原因StampedLock引入了乐观读机制。在“读多写少”的场景下乐观读允许线程在不加锁的情况下读取数据并通过“戳”来验证数据的一致性。这个过程避免了绝大多数读操作的加锁、阻塞和线程切换开销从而极大地提升了读取性能。它是用一种“先上车后补票”的乐观策略换来了性能的飞跃。
StampedLock完整演示代码
package StampLock;import java.util.concurrent.locks.StampedLock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;
import java.util.Random;
import java.util.concurrent.locks.StampedLock;
public class SimpleStampedLockDemo {private static class Point {private double x, y;private final StampedLock lock new StampedLock();// 移动点的位置写操作public void move(double deltaX, double deltaY) {long stamp lock.writeLock();try {x deltaX;y deltaY;System.out.println(Thread.currentThread().getName() 移动点到: ( x , y ));} finally {lock.unlockWrite(stamp);}}// 计算到原点的距离乐观读- 修正版public double distanceFromOrigin() {// 1. 尝试乐观读long stamp lock.tryOptimisticRead();// 2. 读取数据double currentX x;double currentY y;// 模拟读取过程需要一些时间让写线程有机会介入try {Thread.sleep(100); // 模拟复杂计算} catch (InterruptedException e) {e.printStackTrace();}// 3. 验证在读取过程中数据是否被修改if (!lock.validate(stamp)) {// 数据被修改了需要加锁重新读取System.out.println(Thread.currentThread().getName() 乐观读失败升级为悲观读);stamp lock.readLock();try {currentX x;currentY y;} finally {lock.unlockRead(stamp);}} else {System.out.println(Thread.currentThread().getName() 乐观读成功);}return Math.sqrt(currentX * currentX currentY * currentY);}// 专门用于演示乐观读失败的方法public void demonstrateOptimisticReadFailure() {System.out.println(开始演示乐观读失败场景...);// 读线程Thread reader new Thread(() - {long stamp lock.tryOptimisticRead();System.out.println(Thread.currentThread().getName() 获取乐观读戳: stamp);// 读取第一个值double currentX x;System.out.println(Thread.currentThread().getName() 读取 x currentX);// 故意等待让写线程有机会修改数据try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}// 读取第二个值double currentY y;System.out.println(Thread.currentThread().getName() 读取 y currentY);// 验证if (!lock.validate(stamp)) {System.out.println(Thread.currentThread().getName() ❌ 乐观读失败数据在读取过程中被修改了);// 重新用悲观读stamp lock.readLock();try {currentX x;currentY y;System.out.println(Thread.currentThread().getName() 使用悲观读重新读取: ( currentX , currentY ));} finally {lock.unlockRead(stamp);}} else {System.out.println(Thread.currentThread().getName() ✓ 乐观读成功);}}, 读线程);// 写线程Thread writer new Thread(() - {try {// 等待读线程开始Thread.sleep(100);System.out.println(Thread.currentThread().getName() 准备修改数据...);move(10, 10);} catch (InterruptedException e) {e.printStackTrace();}}, 写线程);try {reader.start();writer.start();reader.join();writer.join();} catch (InterruptedException e) {e.printStackTrace();}}// 锁升级示例public void moveToOriginIfInFirstQuadrant() {long stamp lock.readLock();try {if (x 0 y 0) {// 尝试将读锁升级为写锁long writeStamp lock.tryConvertToWriteLock(stamp);if (writeStamp ! 0) {stamp writeStamp;System.out.println(Thread.currentThread().getName() ✓ 成功将读锁升级为写锁);x 0;y 0;} else {System.out.println(Thread.currentThread().getName() ❌ 读锁升级失败重新获取写锁);lock.unlockRead(stamp);stamp lock.writeLock();try {x 0;y 0;} finally {lock.unlockWrite(stamp);return;}}}} finally {lock.unlock(stamp);}}}public static void main(String[] args) throws InterruptedException {Point point new Point();System.out.println( StampedLock 简单示例 \n);// 示例1基本的读写操作System.out.println(1. 基本读写操作);point.move(3, 4);System.out.println(距离原点: point.distanceFromOrigin());System.out.println();// 示例2单线程乐观读肯定成功System.out.println(2. 单线程乐观读肯定成功);double distance point.distanceFromOrigin();System.out.println(距离: distance);System.out.println();// 示例3演示乐观读失败System.out.println(3. 并发场景下的乐观读失败);point.demonstrateOptimisticReadFailure();System.out.println();// 示例4演示锁升级System.out.println(4. 锁升级示例);point.move(5, 5); // 确保在第一象限Thread upgrader new Thread(() - {point.moveToOriginIfInFirstQuadrant();}, 升级线程);upgrader.start();upgrader.join();System.out.println(\n最终位置: ( point.x , point.y ));}
}对代码的疑问之处
当时我对控制台打印的疑问是 sleep不释放锁为什么reader在sleep了200ms之后虽然Writer还有100ms的时间可以获取锁但是reader不释放锁啊为什么Writer还是能获取到锁 其实我是理解错误了在乐观读模式下读线程在调用sleep时根本没有持有任何锁
我们来逐一详细拆解。 问题一为什么 demonstrateOptimisticReadFailure 中写线程能修改成功
您对线程执行顺序的理解出现了一点偏差这也是并发编程初学者最容易遇到的一个困惑点。您可能是这样想的
您的设想串行思路读线程启动 - 读x - 睡200ms - 读y - 结束。然后写线程启动 - 睡100ms - 修改。
但实际情况是t.start() 只是告诉操作系统“这个线程可以开始运行了”但具体什么时候运行、运行多长时间都由 CPU的线程调度器来决定。两个线程一旦 start()就可以看作是在同时、并行地执行。
我们来梳理一下实际的事件时间线 T0ms: main线程调用了 reader.start() 和 writer.start()。此时读线程和写线程都进入了“就绪”状态随时可以被CPU执行。 T~1ms (举例): 读线程抢到了CPU时间片。 它执行 lock.tryOptimisticRead()获取了版本号比如512。它读取了 x 的值3.0。然后它调用 Thread.sleep(200)主动放弃CPU进入了休眠状态。它要等200毫秒后才能醒来。 T~2ms: 写线程抢到了CPU时间片。 它调用 Thread.sleep(100)也主动放弃CPU进入休眠。它只需要等100毫秒。 T~102ms: 写线程的100ms睡眠时间结束了 它被唤醒重新进入“就绪”状态并很快抢到CPU。它调用 move(10, 10)成功获取了写锁因为此时没有其他锁将 x 和 y 修改为 (13.0, 14.0)。写线程的工作完成了。 T~201ms: 读线程的200ms睡眠时间现在才结束 它被唤醒从 sleep(200) 的下一行代码继续执行。它开始读取 y 的值。但此时的 y 已经是被写线程修改后的 14.0。它读取完毕后调用 lock.validate(512)。StampedLock 发现从它获取版本号512到现在中间发生了一次写操作版本号已经变了。因此 validate 返回 false乐观读失败。
结论代码完美地达到了演示失败的目的。正是因为写线程的睡眠时间100ms比读线程的睡眠时间200ms短所以写操作总能发生在读操作的“读取x”和“读取y”这两个动作之间从而导致乐观读验证失败。 问题二锁升级这块不是很理解为什么结果是成功的
我们来深入理解一下 tryConvertToWriteLock 这个“锁升级”操作。
1. 什么是锁升级为什么需要它
想象一个场景你需要先读取一个共享数据根据数据的值再决定是否要修改它。 常规的笨办法 先加读锁。读取数据发现需要修改。释放读锁。再加写锁。问题来了 在你释放读锁和获取写锁的这个“空档期”很可能有另一个线程冲进来修改了数据那你刚才的判断就白费了你必须重新读取和判断非常麻烦。 锁升级的聪明办法 tryConvertToWriteLock 提供了一个在持有读锁的情况下直接尝试转变为写锁的机会中间不释放任何锁从而避免了上述的“空档期”问题。这是一种优化。
2. 锁升级什么时候会成功什么时候会失败
tryConvertToWriteLock 是一个“乐观”的尝试它成功的条件非常苛刻
成功条件当尝试升级时当前线程必须是唯一的读者。也就是说不能有任何其他线程持有读锁。如果StampedLock发现只有你这一个读者它就会很顺利地把你的读锁“升级”成写锁并返回一个新的、代表写锁的“戳”。失败条件只要当时还有任何一个其他线程也持有读锁升级就会立即失败并返回 0。这是为了防止死锁如果两个读线程都想升级成写锁它们会相互等待对方释放读锁从而死锁。
3. 为什么您的代码里升级成功了
我们看一下您的 main 函数中调用这部分的代码
// 示例4演示锁升级
System.out.println(4. 锁升级示例);
point.move(5, 5); // 确保在第一象限
Thread upgrader new Thread(() - {point.moveToOriginIfInFirstQuadrant();
}, 升级线程);
upgrader.start();
upgrader.join();在这里您只创建了一个名为“升级线程”的线程去执行 moveToOriginIfInFirstQuadrant 这个方法。
所以当这个线程执行到 lock.tryConvertToWriteLock(stamp) 时它自己是当前唯一的读者没有任何其他线程持有读锁。因此它完全满足了升级成功的苛刻条件所以升级必然成功并打印出 ✓ 成功将读锁升级为写锁。
4. finally 块中的 lock.unlock(stamp)
您可能会注意到finally 块里只有一个 lock.unlock(stamp)它是如何知道该解锁读锁还是写锁的呢
这也是StampedLock的一个巧妙之处。unlock(stamp) 方法会根据传入的“戳”的类型来自动判断是该执行 unlockRead 还是 unlockWrite。
如果升级失败stamp 变量里保存的还是最初的读锁戳unlock(stamp) 就执行读锁释放。如果升级成功代码 stamp writeStamp; 会把写锁戳赋给 stamp 变量unlock(stamp) 就执行写锁释放。
这种设计简化了 finally 块的逻辑。
总结 为什么叫戳Stamp 每次获取锁都会返回一个唯一的数字戳释放锁时必须提供对应的戳就像票据系统确保锁的正确配对 为什么能提高读性能 乐观读不加锁直接读取性能最高只在数据被修改时才升级为真正的锁适合读多写少的场景 使用场景 读操作远多于写操作对读性能要求很高可以容忍偶尔的读重试 注意事项 不支持重入必须正确管理戳不支持条件变量Condition