# 简介

一般应用,都是读多写少, ReentrantReadWriteLock 因读写互斥,故读时阻塞写,因而性能上上不去。可能会使写线程饥饿, StampedLock 营运而生。

``StampedLock 并没有使用 AQS API 相对复杂,内壁实现比 ReentrantReadWriteLock 复杂得多,所以本文就不对源码进行过多解释。有点就是吞吐量相对 ReentrantReadWriteLock` 有显著提升。

# 原理

获取锁的方法都会返回一个邮戳 StampStamp=0 表示获取失败,其余都表示成功。释放锁时,也需要一个邮戳 Stamp ,这个 Stamp 必须和获得锁时的 Stamp 一致。

看一个简单的例子

class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
    
   void move(double deltaX, double deltaY) {
       long stamp = sl.writeLock();
       try {
           x += deltaX;
           y += deltaY;
       } finally {
           sl.unlockWrite(stamp);// 用到了 Stamp 释放锁
       }
   }
}

# 特点

  • StampedLock 不支持锁重入,写锁的重入会造成死锁。但是支持三种模式:写锁,悲观锁,乐观锁。
  • 只允许一个线程获取写锁,写锁和悲观读锁是互斥的。
  • 允许多 个线程同时获取乐观锁和悲观读锁。

# 使用

我们先来看一下 JDK1.8 源码自带的案例

public class StampedLockDemo {
    private double x,y;
    private final StampedLock sl = new StampedLock();
 
    //【写锁 (排它锁)】
    void move(double deltaX,double deltaY) {
        /**stampedLock 调用 writeLock 和 unlockWrite 时候都会导致 stampedLock 的 stamp 值的变化
         * 即每次 + 1(不是狭义的加一),直到加到最大值,然后从 0 重新开始 
         **/
        long stamp =sl.writeLock(); // 写锁
        try {
            x +=deltaX;
            y +=deltaY;
        } finally {
            sl.unlockWrite(stamp);// 释放写锁
        }
    }
 
    //【乐观读锁】
    double distanceFromOrigin() { // A read-only method
        /**
         * tryOptimisticRead 是一个乐观的读,使用这种锁的读不阻塞写
         * 每次读的时候得到一个当前的 stamp 值(类似时间戳的作用)
         */
        long stamp = sl.tryOptimisticRead();
        // 这里就是读操作,读取 x 和 y,因为读取 x 时,y 可能被写了新的值,所以下面需要判断
        double currentX = x, currentY = y;
        /** 如果读取的时候发生了写,则 stampedLock 的 stamp 属性值会变化,此时需要重读,
         * 再重读的时候需要加读锁(并且重读时使用的应当是悲观的读锁,即阻塞写的读锁)
         * 当然重读的时候还可以使用 tryOptimisticRead,此时需要结合循环了,即类似 CAS 方式
         * 读锁又重新返回一个 stampe 值 */
        if (!sl.validate(stamp)) {// 如果验证失败(读之前已发生写)
            stamp = sl.readLock(); // 悲观读锁
            try {
                currentX = x;
                currentY = y;
            }finally{
                sl.unlockRead(stamp);// 释放读锁
            }
        }
        // 读锁验证成功后执行计算,即读的时候没有发生写
        return Math.sqrt(currentX *currentX + currentY *currentY);
    }
 
    //【悲观读锁】
    void moveIfAtOrigin(double newX, double newY) { // upgrade
        // 读锁(这里可用乐观锁替代)
        long stamp = sl.readLock();
        try {
            // 循环,检查当前状态是否符合
            while (x == 0.0 && y == 0.0) {
            /**
             * 转换当前读戳为写戳,即上写锁
             * 1. 写锁戳,直接返回写锁戳
             * 2. 读锁戳且写锁可获得,则释放读锁,返回写锁戳
             * 3. 乐观读戳,当立即可用时返回写锁戳
             * 4. 其他情况返回 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);
        }
    }
}

# 参考

灰信网:https://www.freesion.com/article/1706212555/

Java 全栈知识体系:https://pdai.tech/md/java/java8/java8-stampedlock.html

https://wizardforcel.gitbooks.io/java8-new-features/content/10.html

https://wizardforcel.gitbooks.io/java8-tutorials/content/Java 8 并发教程 Threads 和 Executors.html