为什么要引入StampedLcok
重入锁、读写锁、StampedLock的并发度对比。:
StampedLock读和写不互斥,并发度更高。StampedLock用乐观读提高并发度。
基本用法:
public class Point {
private double x, y;
private final StampedLock stampedLock = new StampedLock();
void move(double deltaX, double deltaY){ // 修改值的函数,并发情况下多个线程进行调用
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x = deltaX;
y = deltaY;
}finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
double distanceFromOrigin(){
long stamp = stampedLock.tryOptimisticRead(); // 乐观读,得到一个版本号
//取得当前值
double currentX = x;
double currentY = y;
if(!stampedLock.validate(stamp)){ // 用validate检查版本号是否过期,即是否有线程修改数据
stamp = stampedLock.readLock(); // 过期则获取读锁
try {
//重新取得值
currentX = x;
currentY = y;
}
finally {
stampedLock.unlockRead(stamp); // 释放读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
乐观读原理
和读写锁一样,StampedLock也有一个state,而且包含了数据的版本号,因为CAS操作只能操作一个变量。
public class StampedLock implements java.io.Serializable {
private static final int LG_READERS = 7;
// Values for lock state and stamp operations
private static final long RUNIT = 1L;
private static final long WBIT = 1L << LG_READERS; // 第八位表示写锁
private static final long RBITS = WBIT - 1L; // 低七位表示读锁
private static final long RFULL = RBITS - 1L; // 读锁的数量
private static final long ABITS = RBITS | WBIT; // 同时记录写锁和读锁的状态的变量
private static final long SBITS = ~RBITS; // 读锁状态取反
// 锁的初始化状态
private static final long ORIGIN = WBIT << 1;
private transient volatile long state; // 状态值
// 构造函数设置初始化的状态值
public StampedLock() {
state = ORIGIN;
}
}
为什么将state值设为ORIGIN的值,而不是0
原因在于乐观读的实现:
public long tryOptimisticRead() {
long s;
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;// 如果有线程持有写锁返回0
}
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS); // 如果stamp为0返回false,因为state第九位为1,state & SBITS不为0
}
当(s = state) & WBIT != 0时(也就是有线程持有写锁时),tryOptimisticRead() 返回0,在调用validate(0)时,会返回false。也就是说当有线程持有写锁后,即使释放了写锁,也认为数据被修改(state低第八位为0,但是低第九位为1,两个数&SBITS的结果不同),所以validate返回false。
而如果将state初始为0的话,当一个读线程在调用validate前,持有写锁的线程释放了写锁,validate会返回true(因为stamp和state均为0),此时不符合逻辑。
为什么vaildate比较(stamp & SBITS) == (state & SBITS),而不是比较stamp和state:因为读锁和读锁之间不互斥,当state和stamp低七位不同时,仍然返回true。
在validate中用了U.loadFence()内存屏障,因为stamp和SBITS不是volatile的,用内存屏障禁止和前面的取值操作重排序。
悲观读写:
StampedLock实现悲观读写不是基于AQS,而是重新实现了一个阻塞队列,内部有一个静态内部类表示阻塞线程节点。
static final class WNode {
volatile WNode prev;
volatile WNode next;
volatile WNode cowait;
volatile Thread thread;
volatile int status;// 取值为0、WARNING、CANCELLED
final int mode;// 取值为RMODE或WMODE
WNode(int m, WNode p) { mode = m; prev = p; }
}
private transient volatile WNode whead;
private transient volatile WNode wtail;
阻塞队列实现类似于AQS,初始时,头结点和尾结点为NULL,然后创建空节点往队列的尾部加入线程节点。不同在于,AQS中CAS修改线程状态失败后,会将节点加入阻塞队列,而该队列是进行自旋,自旋一定次数仍取不到锁再进入阻塞。自旋的次数由CPU核心数决定:
private static final int NCPU = Runtime.getRuntime().availableProcessors();
private static final int SPINS = (NCPU > 1) ? 1 << 6 : 0;
以写锁为例查看自旋的实现:
public long writeLock() {
long s, next;
return ((((s = state) & ABITS) == 0L &&
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
next : acquireWrite(false, 0L));
}
只有没有线程持有锁(读锁和写锁)才能修改state值((s = state) & ABITS) == 0L)。若操作失败进入阻塞队列,并自旋。
private static final int SPINS = (NCPU > 1) ? 1 << 6 : 0;
// 进入队列前最大重试次数
private static final int HEAD_SPINS = (NCPU > 1) ? 1 << 10 : 0;
// 在队头阻塞前的最大重试次数
private static final int MAX_HEAD_SPINS = (NCPU > 1) ? 1 << 16 : 0;
/** The period for yielding when waiting for overflow spinlock */
private static final int OVERFLOW_YIELD_RATE = 7; // must be power 2 - 1
/** The number of bits to use for reader count before overflowing */
private static final int LG_READERS = 7;
private long acquireWrite(boolean interruptible, long deadline) {
WNode node = null, p;
for (int spins = -1;;) { // 入队自旋
long m, s, ns;
if ((m = (s = state) & ABITS) == 0L) {
if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
return ns; // 自旋时拿到锁,函数返回
}
else if (spins < 0)
spins = (m == WBIT && wtail == whead) ? SPINS : 0;
else if (spins > 0) {
if (LockSupport.nextSecondarySeed() >= 0) // 自旋,在一定概率下将spins递减
--spins;
}
else if ((p = wtail) == null) { // 初始化队列
WNode hd = new WNode(WMODE, null);
if (U.compareAndSwapObject(this, WHEAD, null, hd))
wtail = hd;
}
else if (node == null)
node = new WNode(WMODE, p);
else if (node.prev != p)
node.prev = p;
else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
p.next = node;
break; // CAS成功将线程加入队列尾部退出循环
}
}
for (int spins = -1;;) {
WNode h, np, pp; int ps;
if ((h = whead) == p) {
if (spins < 0)
spins = HEAD_SPINS;
else if (spins < MAX_HEAD_SPINS)
spins <<= 1;
for (int k = spins;;) { // 队头重试
long s, ns;
if (((s = state) & ABITS) == 0L) { // 再次尝试获取锁
if (U.compareAndSwapLong(this, STATE, s,
ns = s + WBIT)) {
whead = node;
node.prev = null;
return ns;
}
}
else if (LockSupport.nextSecondarySeed() >= 0 &&
--k <= 0) // 自旋
break;
}
}
else if (h != null) { // 帮助释放旧的等待线程
WNode c; Thread w;
while ((c = h.cowait) != null) { // 结束阻塞,然后唤醒cowaiter在的reader线程
if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
}
if (whead == h) {
if ((np = node.prev) != p) {
if (np != null)
(p = np).next = node; // stale
}
else if ((ps = p.status) == 0)
U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
else if (ps == CANCELLED) {
if ((pp = p.prev) != null) {
node.prev = pp;
pp.next = node;
}
}
else {
long time; // 参数为0表示未超时
if (deadline == 0L)
time = 0L;
else if ((time = deadline - System.nanoTime()) <= 0L)
return cancelWaiter(node, node, false);
Thread wt = Thread.currentThread();
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
whead == h && node.prev == p)
U.park(false, time); // 进入阻塞
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
if (interruptible && Thread.interrupted())
return cancelWaiter(node, node, true);
}
}
}
}
第一个大循环里,一边尝试获得锁(如果获取到锁就返回),一边把线程Node放到队列尾部,一直自旋到节点放到队列尾部。
在第二个循环里,如果当前线程在队列头部,则进行新的一轮自旋,次数上限为MAX_HEAD_SPINS,如果获得了锁则返回,否则阻塞。
当cowaiter的头部线程被唤醒时,会顺带唤醒所有读线程。
释放锁的操作:
public void unlockWrite(long stamp) {
WNode h;
if (state != stamp || (stamp & WBIT) == 0L) // 锁版本不正确或没有线程获取到锁
throw new IllegalMonitorStateException();
state = (stamp += WBIT) == 0L ? ORIGIN : stamp; // 将state归位
if ((h = whead) != null && h.status != 0) // 唤醒队头节点
release(h);
}
private void release(WNode h) {
if (h != null) {
WNode q; Thread w;
U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
if ((q = h.next) == null || q.status == CANCELLED) {
for (WNode t = wtail; t != null && t != h; t = t.prev) // 往前寻找头结点
if (t.status <= 0)
q = t;
}
if (q != null && (w = q.thread) != null) // 唤醒线程
U.unpark(w);
}
}
参考资料:《Java并发实现原理:JDK源码剖析》