java并发:线程并发控制机制之StampedLock

StampedLock

通常情况下,使用同步锁的代码如下:

synchronized(this){
    // do operation
}

Java6提供的ReentrantReadWriteLock使用方式如下:

rwlock.writeLock().lock();
try {
    // do operation
} finally {
    rwlock.writeLock().unlock();
}

ReentrantLock、ReentrantReadWriteLock和synchronized有相同的内存语义;相对而言,synchronized代码要更容易书写,而使用ReentrantLock的代码必须严格按照一定的方式来写,否则就会造成其他严重的问题。

Java8引入了一个新的读写锁:StampedLock,它提供强大的乐观读锁API,这意味着你能以一个较低的代价获得一个读锁,适用于读多写少的场景。

关于这里“乐观读锁”的含义请参见后文描述。

 

性能对比

StampedLock 专为超高吞吐量的读操作优化(如金融行情处理),其设计舍弃了复杂功能以换取性能:

image

 

image

 

补充:

Condition 的典型用法是嵌套锁获取

lock.lock();
try {
    while (!condition) {
        condition.await(); // 释放锁并等待
    }
} finally {
    lock.unlock();
}

解读:在 StampedLock 中,由于不可重入,若在 await() 中释放锁后再次获取锁,会引发死锁;禁止 Condition 可规避此类风险。

若支持 Condition,每次锁操作需维护等待队列,导致:

读操作延迟增加 20%~30%(实测)
写锁获取的 CAS 操作复杂度上升

 

示例

账户余额系统

public class AccountBalance {
    private double balance;
    private final StampedLock lock = new StampedLock();

    // 写操作:存款(独占写锁)
    public void deposit(double amount) {
        long stamp = lock.writeLock(); // 获取写锁
        try {
            balance += amount;
        } finally {
            lock.unlockWrite(stamp); // 释放写锁
        }
    }

    // 乐观读:查询余额(无锁竞争)
    public double getBalance() {
        long stamp = lock.tryOptimisticRead(); // 尝试乐观读
        double currentBalance = balance;
        if (!lock.validate(stamp)) { // 检查是否被写操作干扰
            stamp = lock.readLock(); // 退化为悲观读锁
            try {
                currentBalance = balance;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return currentBalance;
    }

    // 锁降级:修改后立即读取
    public void updateAndLog() {
        long stamp = lock.writeLock();
        try {
            balance = calculateNewBalance(); // 写操作
            // 降级为读锁(保持修改后的状态)
            long readStamp = lock.tryConvertToOptimisticRead(stamp); 
            logCurrentBalance(); // 读操作
        } finally {
            lock.unlock(readStamp);
        }
    }
}

关键点解析:

乐观读(tryOptimisticRead)

直接读取数据,通过 validate(stamp) 验证数据一致性(类似 CAS 机制)。
若验证失败(期间有写操作),则退化为悲观读锁。

 

锁降级(tryConvertToOptimisticRead)

写锁完成后,转换为乐观读锁,避免其他写操作干扰,同时允许并发读。

 

实时数据统计

public class VisitCounter {
    private int count;
    private StampedLock lock = new StampedLock();
    
    // 高并发下累加访问量
    public void increment() {
        long stamp = lock.writeLock();
        try { count++; } finally { lock.unlockWrite(stamp); }
    }
    
    // 实时读取(无需阻塞)
    public int getCount() { /* 同前文乐观读逻辑 */ }
}

 

缓存系统更新

public void refreshCache() {
    long stamp = lock.writeLock();
    try {
        loadNewCache(); // 写操作
        stamp = lock.tryConvertToOptimisticRead(stamp); // 降级
    } finally {
        lock.unlock(stamp);
    }
}

解读:使用锁降级保证缓存更新后立即可读

 

源码解读

image

 

StampedLock的定义如下:

public class StampedLock implements java.io.Serializable {

Note:

在该类的定义文件中列举出了StampedLock的各种使用示例。

构造函数

StampedLock只提供了一个构造函数,代码如下:

    /**
     * Creates a new lock, initially in unlocked state.
     */
    public StampedLock() {
        state = ORIGIN;
    }

 

其类图如下:

 

StampedLock有三种模式(写、悲观读、乐观读)

  • 写锁(writeLock)

writeLock是一个排它锁或者独占锁,某个时间只有一个线程可以获取该锁,当一个线程获取该锁后,其他请求读锁和写锁的线程必须等待,这类似于 ReentrantReadWriteLock 的写锁(不同的是这里的写锁是不可重入锁)。

请求该锁成功后会返回一个 stamp 变量用来表示该锁的版本,当释放该锁时需要调用 unlockWrite 方法并传递获取锁时得到的 stamp 作为参数。

  • 悲观读锁(readLock)

readLock是一个共享锁,在没有线程获写锁的情况下,多个线程可以同时获取该锁,这类似于 ReentrantReadWriteLock 的读锁 (不同的是这里的读锁是不可重入锁)。

请求该锁成功后会返回一个 stamp变量用来表示该锁的版本,当释放该锁时需要调用 unlockRead 方法并传递获取锁时得到的 stamp 作为参数。

进一步解释这里“悲观”的含义:

这里说的悲观是指在具体操作数据前其会悲观地认为其他线程可能要对当前操作数据进行修改,所以先对数据加锁,这是在读少写多的情况下的一种考虑。

  • 乐观读锁(tryOptimisticRead)

它是相对于悲观锁来说的,它在操作数据前并没有通过 CAS 设置锁的状态。

如果当前没有线程持有写锁,则简单地返回一个非 0 的 stamp 变量用来表示该锁的版本;在具体操作数据前需要调用 validate 方法验证该 stamp 是否己经不可用。

Note:

(1)由于 tryOptimisticRead 并没有使用 CAS 设置锁状态,所以不需要显式地释放锁。

(2)该锁适用于读多写少的场景,因为获取读锁时仅使用位操作进行检验,不涉及 CAS 操作,所以效率会高很多;但由于没有使用真正的锁,在保证数据一致性上需要复制一份变量到方法栈,且在操作数据时可能其它写线程己经修改了数据,所以返回的不是最新数据,但是最终一致性还是得到了保障。

 

使用乐观读锁很容易犯错,必须要遵循如下的使用顺序

 

注意事项

不可重入

同一线程重复获取锁会导致死锁(需设计避免嵌套锁)。

void deadlockExample() {
    long stamp1 = lock.readLock(); 
    long stamp2 = lock.writeLock(); // 阻塞!因读锁未释放
    lock.unlockRead(stamp1); // 永远无法执行
}

解读:强制开发者避免嵌套锁获取(如持有读锁时再申请写锁),防止死锁风险

 

StampedLock 不支持锁重入是其设计上的主动选择,主要出于性能优化和实现简洁性的考量。 

技术原理:从锁状态结构解析

StampedLock 的核心是一个 64 位 long 型状态变量 state,其结构如下:

// state 的位结构:
private static final long WBIT = 1L << 7;  // 第 8 位标记写锁
private static final long RBITS = WBIT - 1; // 低 7 位标记读锁计数
private volatile long state; // 锁状态

写锁:仅用 WBIT 位表示(0 或 1),无重入计数空间
读锁:低 7 位记录读线程数量(最多 127 个),但不记录线程 ID
乐观读:通过 state 的高位生成版本戳(Stamp)

关键限制 —— 若支持重入,需额外存储:

A、当前持有锁的线程 ID
B、每个线程的重入次数
这将破坏 state 的紧凑结构,增加内存和计算开销。

 

问题:为何牺牲重入性?

无重入的锁获取:仅需一次原子操作(CAS),无需判断当前线程是否已持有锁。

public long writeLock() {
    long s, next;
    return ((s = state) & WBIT) == 0L ? next = s + WBIT : 0L;
}

 

重入实现:引入线程判断和计数维护后,锁操作延迟显著增加

public long writeLock() {
    if (currentThread == holderThread) { // 需额外存储线程信息
        reentrantCount++; // 需维护重入计数
        return generateNewStamp(); // 需生成新戳记
    }
    // 原逻辑...
}

 

重入需求替代方案

1、使用 tryConvertToWriteLock() 等转换方法替代嵌套获取

long stamp = lock.readLock();
try {
    if (needWrite) {
        stamp = lock.tryConvertToWriteLock(stamp); // 非嵌套式升级
        if (stamp == 0) {
            stamp = lock.writeLock(); // 转换失败则显式获取
        }
    }
} finally {
    lock.unlock(stamp);
}

 

2、分层锁设计 —— 对重入场景使用 ReentrantLock 作为外层锁

private final ReentrantLock outerLock = new ReentrantLock();
private final StampedLock innerLock = new StampedLock();

void safeNestedOperation() {
    outerLock.lock(); // 可重入外层
    try {
        long stamp = innerLock.writeLock(); // 内层高性能锁
        try { /* 操作 */ } finally { innerLock.unlockWrite(stamp); }
    } finally { outerLock.unlock(); }
}

 

锁升级

StampedLock 支持三种锁在一定条件下进行相互转换,例如:long tryConvertToWriteLock(long stamp) 期望把 stamp 标示的锁升级为写锁。

该函数会在下面几种情况下返回一个有效的 stamp (也就是晋升写锁成功):

  • 当前锁己经是写锁模式
  • 当前锁处于读锁模式,并且没有其他线程是读锁模式

 

实现条件等待

1、组合使用 ReentrantLock
将 StampedLock 与 ReentrantLock 搭配,由后者管理条件队列:

public class HybridLockSystem {
    private final StampedLock stampedLock = new StampedLock();
    private final ReentrantLock conditionLock = new ReentrantLock();
    private final Condition condition = conditionLock.newCondition();

    public void conditionalWrite() {
        conditionLock.lock();
        try {
            long stamp = stampedLock.writeLock();
            try {
                while (!dataReady()) {
                    condition.await(); // 使用 ReentrantLock 的条件等待
                }
                processData();
            } finally {
                stampedLock.unlockWrite(stamp);
            }
        } finally {
            conditionLock.unlock();
        }
    }
}

 

2、自旋 + 轮询(适用于低延迟场景)

对状态变化频繁的场景,可用乐观读轮询替代阻塞等待:

public double pollUntilCondition() {
    long stamp;
    double currentValue;
    do {
        stamp = lock.tryOptimisticRead();
        currentValue = sharedValue;
        if (lock.validate(stamp)) {
            break; // 验证通过
        }
        Thread.yield(); // 避免忙等待
    } while (true);
    return currentValue;
}

 

总结

StampedLock 通过牺牲重入性,换取了极致的轻量级操作和更高的吞吐量。

开发者需根据场景权衡选择,或通过锁转换/分层设计满足重入需求。

posted @ 2021-08-13 21:12  时空穿越者  阅读(129)  评论(0)    收藏  举报