之前在ReadWriteLock中我们说读锁的时候不允许写锁的存在,也就是不能出现写的操作,因为这样会让读的线程获取的数据为脏数据。为了提升并发执行效率,java8中引入了新的读写锁StampedLock.
相比与ReadWriteLock,改进之处在于读得过程中允许获取写锁的后写入,这样我们读的数据就不可能不一致。所以相对于ReadWriteLock来说StampedLock具有更高的并发效率。和ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取。首先通过tryOptimisticRead获取一个乐观锁。并返回版本号,接着进行读取,读取完毕之后,通过validate来验证版本号。如果在读取的时候版本号没有发生改变,就可以继续进行读操作,如果版本号发生改变说明就有其他的线程进行了修改,此时就需要获取悲观锁进行重新读取。但是程序在大多数情况下写入的概率不高,也就是很少使用悲观锁。因此这样可减少加锁的成本。提升系统的性能。
StempedLock将锁分为乐观锁和悲观锁,能够提升程序的并发效率,但是代码却变得复杂,StampedLock是不可重入的锁不能再一个线程中循环反复的获取同一个锁。
StempedLock还提供了复杂的将悲观锁升级为写锁的功能,即先读,如果读的数据满足条件,就返回,如果读的数据不满足条件,就再尝试写。
在阅读源码之前,我们还是按照之前的规则,首先是从调用开始。
// Unsafe mechanics
private static final sun.misc.Unsafe U;
private static final long STATE;
private static final long WHEAD;
private static final long WTAIL;
private static final long WNEXT;
private static final long WSTATUS;
private static final long WCOWAIT;
private static final long PARKBLOCKER;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = StampedLock.class;
Class<?> wk = WNode.class;
STATE = U.objectFieldOffset
(k.getDeclaredField("state"));
WHEAD = U.objectFieldOffset
(k.getDeclaredField("whead"));
WTAIL = U.objectFieldOffset
(k.getDeclaredField("wtail"));
WSTATUS = U.objectFieldOffset
(wk.getDeclaredField("status"));
WNEXT = U.objectFieldOffset
(wk.getDeclaredField("next"));
WCOWAIT = U.objectFieldOffset
(wk.getDeclaredField("cowait"));
Class<?> tk = Thread.class;
PARKBLOCKER = U.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
} catch (Exception e) {
throw new Error(e);
}
}
首先还是Unsafe类,可以看出StampedLock还是通过操作内存来进行锁操作。因为每个线程都有一份代码,但这些变量是static修饰,因此具有唯一性,也就是每个线程对这些变量的修改对其他线程来说是可见的。
但是我们发现StampedLock并没有使用AQS来进行多线程的调度,然后自己实现了一套。这可能和java8的新增有关系。
根据上边的描述,我们看一下StampedLock的加锁过程。
public long tryReadLock() {
for (;;) {
long s, m, next;
if ((m = (s = state) & ABITS) == WBIT)
return 0L;
else if (m < RFULL) {
if (U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))
return next;
}
else if ((next = tryIncReaderOverflow(s)) != 0L)
return next;
}
}
这里用过Unsafe的CAS去将state的值进行加法操作,按着这里的意思就是有一个上限,如果没有达到上限就用加法,否则就用tryIncReaderOverflow方法进行生成。通过查看StampedLock的源码,发现基本都是通过Unsafe去操作内存的。也没有AQS那么复杂,但是对内存的操作和相关逻辑在StampedLock确实比较复杂。那么它是如何释放锁的时候激活其他其他线程的?
public void unlock(long stamp) {
long a = stamp & ABITS, m, s; WNode h;
while (((s = state) & SBITS) == (stamp & SBITS)) {
if ((m = s & ABITS) == 0L)
break;
else if (m == WBIT) {
if (a != m)
break;
state = (s += WBIT) == 0L ? ORIGIN : s;
if ((h = whead) != null && h.status != 0)
release(h);
return;
}
else if (a == 0L || a >= WBIT)
break;
else if (m < RFULL) {
if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
if (m == RUNIT && (h = whead) != null && h.status != 0)
release(h);
return;
}
}
else if (tryDecReaderOverflow(s) != 0L)
return;
}
throw new IllegalMonitorStateException();
}
我们看这里whead是StampedLock阻塞队列的头,那么realse就是释放当前线程,然后激活其他线程了。
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);
}
}
在release方法中,最终调用的是Unsafe的激活w线程的方法。
在申请读锁要考虑写锁的情况,在写锁的时候要考虑读锁的情况,所以在已经加加锁的情况下对不允许的线程就需要将其添加到等待队列里。具体在
acquireWrite(boolean interruptible, long deadline)
acquireRead(boolean interruptible, long deadline)
这两个函数主要就是要筛选不符合条件的线程加入到wnode线程链表中,在unlock之后然后激活。由于这两块代码比较复杂,我们还是以知道其作用的想法去看待,而不取纠结其具体的实现过程。
总结
StampedLock提供了乐观读锁,能够替代ReadWriteLock提升并发性能。
StampedLock是不可重入锁。