Java并发(8)- 读写锁中的性能之王:StampedLock (2)

上面的源码中除了定义state变量外,还提供了一系列变量用来操作state,用来表示读锁和写锁的各种状态。为了方便理解,我将他们都表示成二进制的值,长度有限,这里用低12位来表示64的long,高位自动用0补齐。要理解这些状态的作用,就需要具体分析三种锁操作方式是怎么通过state这一个变量来表示的,首先来看看获取写锁和释放写锁。

源码分析:写锁的释放和获取 public StampedLock() { state = ORIGIN; //初始化state为 0001 0000 0000 } public long writeLock() { long s, next; return ((((s = state) & ABITS) == 0L && //没有读写锁 U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ? //cas操作尝试获取写锁 next : acquireWrite(false, 0L)); //获取成功后返回next,失败则进行后续处理,排队也在后续处理中 } public void unlockWrite(long stamp) { WNode h; if (state != stamp || (stamp & WBIT) == 0L) //stamp值被修改,或者写锁已经被释放,抛出错误 throw new IllegalMonitorStateException(); state = (stamp += WBIT) == 0L ? ORIGIN : stamp; //加0000 1000 0000来记录写锁的变化,同时改变写锁状态 if ((h = whead) != null && h.status != 0) release(h); }

这里先说明两点结论:读锁通过前7位来表示,每获取一个读锁,则加1。写锁通过除前7位后剩下的位来表示,每获取一次写锁,则加1000 0000,这两点在后面的源码中都可以得倒证明。
初始化时将state变量设置为0001 0000 0000。写锁获取通过((s = state) & ABITS)操作等于0时默认没有读锁和写锁。写锁获取分三种情况:

没有读锁和写锁时,state为0001 0000 0000
0001 0000 0000 & 0000 1111 1111 = 0000 0000 0000 // 等于0L,可以尝试获取写锁

有一个读锁时,state为0001 0000 0001
0001 0000 0001 & 0000 1111 1111 = 0000 0000 0001 // 不等于0L

有一个写锁,state为0001 1000 0000
0001 1000 0000 & 0000 1111 1111 = 0000 1000 0000 // 不等于0L

获取到写锁,需要将s + WBIT设置到state,也就是说每次获取写锁,都需要加0000 1000 0000。同时返回s + WBIT的值
0001 0000 0000 + 0000 1000 0000 = 0001 1000 0000

释放写锁首先判断stamp的值有没有被修改过或者多次释放,之后通过state = (stamp += WBIT) == 0L ? ORIGIN : stamp来释放写锁,位操作表示如下:
stamp += WBIT
0010 0000 0000 = 0001 1000 0000 + 0000 1000 0000
这一步操作是重点!!!写锁的释放并不是像ReentrantReadWriteLock一样+1然后-1,而是通过再次加0000 1000 0000来使高位每次都产生变化,为什么要这样做?直接减掉0000 1000 0000不就可以了吗?这就是为了后面乐观锁做铺垫,让每次写锁都留下痕迹。

大家可以想象这样一个场景,字母A变化为B能看到变化,如果在一段时间内从A变到B然后又变到A,在内存中自会显示A,而不能记录变化的过程,这也就是CAS中的ABA问题。在StampedLock中就是通过每次对高位加0000 1000 0000来达到记录写锁操作的过程,可以通过下面的步骤理解:
第一次获取写锁:
0001 0000 0000 + 0000 1000 0000 = 0001 1000 0000
第一次释放写锁:
0001 1000 0000 + 0000 1000 0000 = 0010 0000 0000
第二次获取写锁:
0010 0000 0000 + 0000 1000 0000 = 0010 1000 0000
第二次释放写锁:
0010 1000 0000 + 0000 1000 0000 = 0011 0000 0000
第n次获取写锁:
1110 0000 0000 + 0000 1000 0000 = 1110 1000 0000
第n次释放写锁:
1110 1000 0000 + 0000 1000 0000 = 1111 0000 0000
可以看到第8位在获取和释放写锁时会产生变化,也就是说第8位是用来表示写锁状态的,前7位是用来表示读锁状态的,8位之后是用来表示写锁的获取次数的。这样就有效的解决了ABA问题,留下了每次写锁的记录,也为后面乐观锁检查变化提供了基础。

关于acquireWrite方法这里不做具体分析,方法非常复杂,感兴趣的同学可以网上搜索相关资料。这里只对该方法做下简单总结,该方法分两步来进行线程排队,首先通过随机探测的方式多次自旋尝试获取锁,然后自旋一定次数失败后再初始化节点进行插入。

源码分析:悲观读锁的释放和获取 public long readLock() { long s = state, next; return ((whead == wtail && (s & ABITS) < RFULL && //队列为空,无写锁,同时读锁未溢出,尝试获取读锁 U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ? //cas尝试获取读锁+1 next : acquireRead(false, 0L)); //获取读锁成功,返回s + RUNIT,失败进入后续处理,类似acquireWrite } public void unlockRead(long stamp) { long s, m; WNode h; for (;;) { if (((s = state) & SBITS) != (stamp & SBITS) || (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT) throw new IllegalMonitorStateException(); if (m < RFULL) { //小于最大记录值(最大记录值127超过后放在readerOverflow变量中) if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) { //cas尝试释放读锁-1 if (m == RUNIT && (h = whead) != null && h.status != 0) release(h); break; } } else if (tryDecReaderOverflow(s) != 0L) //readerOverflow - 1 break; } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zydggf.html