面试题 / Java

StampedLock

flowchart TB
    subgraph StampedLock["StampedLock(JDK1.8+)"]
        style StampedLock fill:#F0F2F5,stroke:#E0E6ED,rx:10,ry:10
        subgraph Modes["模式分类"]
            style Modes fill:#F5F7FA,stroke:#E0E6ED,rx:10,ry:10
            Write(["写锁(独占):单线程持有,阻塞其他读写"]):::write
            Read(["读锁(悲观读):无写锁时多线程共享"]):::read
            Optimistic(["乐观读:无写锁时直接访问,提交时验证"]):::optimistic
        end
        subgraph Features["核心特点"]
            style Features fill:#F5F7FA,stroke:#E0E6ED,rx:10,ry:10
            F1(["不可重入,不支持Condition"]):::feature
            F2(["性能优秀(乐观读减少阻塞)"]):::feature
            F3(["适用场景:读多写少,无重入需求"]):::feature
        end
    end

    classDef write fill:#C44545,color:#fff,rx:10,ry:10
    classDef read fill:#00838F,color:#fff,rx:10,ry:10
    classDef optimistic fill:#4CA497,color:#fff,rx:10,ry:10
    classDef feature fill:#E99151,color:#333,rx:10,ry:10

    linkStyle default stroke-width:1.5px,opacity:0.8

StampedLock 面试中问的比较少,不是很重要,简单了解即可。

StampedLock 是什么?

StampedLock 是 JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 Condition

不同于一般的 Lock 类,StampedLock 并不是直接实现 LockReadWriteLock接口,而是基于 CLH 锁 独立实现的(AQS 也是基于这玩意)。

public class StampedLock implements java.io.Serializable {
}

StampedLock 提供了三种模式的读写控制模式:读锁、写锁和乐观读。

  • 写锁:独占锁,一把锁只能被一个线程获得。当一个线程获取写锁后,其他请求读锁和写锁的线程必须等待。类似于 ReentrantReadWriteLock 的写锁,不过这里的写锁是不可重入的。
  • 读锁 (悲观读):共享锁,没有线程获取写锁的情况下,多个线程可以同时持有读锁。如果己经有线程持有写锁,则其他线程请求获取该读锁会被阻塞。类似于 ReentrantReadWriteLock 的读锁,不过这里的读锁是不可重入的。
  • 乐观读:允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。

另外,StampedLock 还支持这三种锁在一定条件下进行相互转换 。

long tryConvertToWriteLock(long stamp){}
long tryConvertToReadLock(long stamp){}
long tryConvertToOptimisticRead(long stamp){}

StampedLock 在获取锁的时候会返回一个 long 型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳,这也是StampedLock不可重入的原因。

// 写锁
public long writeLock() {
    long s, next;  // bypass acquireWrite in fully unlocked case only
    return ((((s = state) & ABITS) == 0L &&
             U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
            next : acquireWrite(false, 0L));
}
// 读锁
public long readLock() {
    long s = state, next;  // bypass acquireRead on common uncontended case
    return ((whead == wtail && (s & ABITS) < RFULL &&
             U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
            next : acquireRead(false, 0L));
}
// 乐观读
public long tryOptimisticRead() {
    long s;
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

StampedLock 的性能为什么更好?

相比于传统读写锁多出来的乐观读是StampedLockReadWriteLock 性能更好的关键原因。StampedLock 的乐观读允许一个写线程获取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了线程饥饿的问题,吞吐量大大提高。

StampedLock 适合什么场景?

ReentrantReadWriteLock 一样,StampedLock 同样适合读多写少的业务场景,可以作为 ReentrantReadWriteLock的替代品,性能更好。

不过,需要注意的是StampedLock不可重入,不支持条件变量 Condition,对中断操作支持也不友好(使用不当容易导致 CPU 飙升)。如果你需要用到 ReentrantLock 的一些高级性能,就不太建议使用 StampedLock 了。

另外,StampedLock 性能虽好,但使用起来相对比较麻烦,一旦使用不当,就会出现生产问题。强烈建议你在使用StampedLock 之前,看看 StampedLock 官方文档中的案例

StampedLock 的底层原理了解吗?

StampedLock 不是直接实现 LockReadWriteLock接口,而是基于 CLH 锁 实现的(AQS 也是基于这玩意),CLH 锁是对自旋锁的一种改良,是一种隐式的链表队列。StampedLock 通过 CLH 队列进行线程的管理,通过同步状态值 state 来表示锁的状态和类型。

StampedLock 的原理和 AQS 原理比较类似,这里就不详细介绍了,感兴趣的可以看看下面这两篇文章:

如果你只是准备面试的话,建议多花点精力搞懂 AQS 原理即可,StampedLock 底层原理在面试中遇到的概率非常小。


来源引用