存在的意义
呃,StampedLock的存在有一部分是为了替换ReentrantedReadWriteLock,StampedLock不是基于AQS实现的,其效率比AQS的读写锁高。
那他和ReentrantedReadWriteLock的区别呢?
1、实现原理不一样,一个是基于AQS,StampedLock是自己内部实现。
2、同样具有读写锁的特点,但是与ReentrantedReadWriteLock不通的是,StempedLock的写锁是不可以重入的。
3、RentrantedReadWriteLock支持公平和非公平,但是StampedLock只支持公平锁
4、StampedLock支持乐观锁。
特性
读读之间的不互斥
public class StampedLockTest {
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
StampedLock stampedLock = new StampedLock();
Lock readLock = stampedLock.asReadLock();
Lock writeLock = stampedLock.asWriteLock();
CyclicBarrier cyclicBarrier = new CyclicBarrier(6);
long start = System.currentTimeMillis();
Runnable run = ()->{
try {
readLock.lock();
TimeUnit.SECONDS.sleep(1);
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}finally {
readLock.unlock();
}
};
for (int i = 0; i < 5; i++) {
new Thread(run).start();
}
cyclicBarrier.await();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
写写互斥
public class StampedLockTest {
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
StampedLock stampedLock = new StampedLock();
// Lock readLock = stampedLock.asReadLock();
Lock writeLock = stampedLock.asWriteLock();
CyclicBarrier cyclicBarrier = new CyclicBarrier(6);
long start = System.currentTimeMillis();
Runnable run = ()->{
try {
writeLock.lock();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
writeLock.unlock();
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 5; i++) {
new Thread(run).start();
}
cyclicBarrier.await();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
读写互斥,写读互斥 —(悲观读)
public class StampedLockTest {
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
StampedLock stampedLock = new StampedLock();
Lock readLock = stampedLock.asReadLock();
Lock writeLock = stampedLock.asWriteLock();
CountDownLatch countDownLatch = new CountDownLatch(2);
long start = System.currentTimeMillis();
new Thread(() -> {
try {
writeLock.lock();
TimeUnit.SECONDS.sleep(1);
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}).start();
new Thread(() -> {
try {
readLock.lock();
TimeUnit.SECONDS.sleep(1);
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}).start();
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
死锁问题, 先拿到写锁就去拿读锁(ReentrantedReadWriteLock是可以进行锁降级)
SteampedLock, 拥有写锁去获取读锁或者拥有读锁去获取写锁都会造成死锁问题。
重入问题
读锁可以重入,写锁不可以重入。
StampedLock支持乐观锁
正常的读写锁
public class OptimisticLock {
static int a = 0;
public static void main(String[] args) throws InterruptedException {
StampedLock stampedLock = new StampedLock();
Lock writeLock = stampedLock.asWriteLock();
Lock readlock = stampedLock.asReadLock();
CountDownLatch countDownLatch = new CountDownLatch(1);
CountDownLatch other = new CountDownLatch(11);
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i1 = 0; i1 < 1000000; i1++) {
try {
readlock.lock();
int b = a;
} finally {
readlock.unlock();
}
}
other.countDown();
}).start();
}
new Thread(() -> {
try {
countDownLatch.await();
for (int i = 0; i < 1000000; i++) {
try {
writeLock.lock();
a++;
} finally {
writeLock.unlock();
}
}
other.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
countDownLatch.countDown();
other.await();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
2342
乐观读
public class OptimisticLock {
static int a = 0;
public static void main(String[] args) throws InterruptedException {
StampedLock stampedLock = new StampedLock();
CountDownLatch countDownLatch = new CountDownLatch(1);
CountDownLatch other = new CountDownLatch(11);
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i1 = 0; i1 < 1000000; i1++) {
long l = stampedLock.tryOptimisticRead();
int b = a;
if (!stampedLock.validate(l)) {
long l1 = 0;
try {
l1 = stampedLock.readLock();
b = a;
} finally {
stampedLock.unlock(l1);
}
}
}
other.countDown();
}).start();
}
new Thread(() -> {
try {
countDownLatch.await();
for (int i = 0; i < 1000000; i++) {
long l = 0;
try {
l = stampedLock.writeLock();
a++;
} finally {
stampedLock.unlock(l);
}
}
other.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
countDownLatch.countDown();
other.await();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
240
思考这么快的原因:
呃,如果写锁获取锁之后 一直不修改。那么这个过程其实是不是可以去读的,应该值一直不变,或者或者写锁获取锁的时间间隔,值也没有被修改,也可以去读,所以乐观读,能极大的提升效率,当然乐观读失败要么加锁变成悲观读,要么循环继续乐观读, 如果循环一直乐观失败就会浪费cpu,所以循环里面可以加点睡眠一下。
state的结构
呃, ReentrantReadWriteLock的高16位是读锁,低16位是写锁, 但是StampedLock里面的state是long型的, 高56位表示版本号,低8位标识读锁和写锁, 在低8位中, 第8位为0表示读锁,1表示写锁。由于StampedLock是不可以重入的所以写锁的时候低8位为 10000000, 写锁释放之后版本号加1, 也就是低state+10000000, 这样往前进一位,本版号就加一了。
呃呃,写锁是可以共享的, 低8位表示读锁, 除去标识位,就只用7位可以用来代表读锁的数量,所以理论读锁最大值为2的8次方 -1。这才几个线程,所以如果读锁在低8位计数满了之后会把读锁的次数进行挪动。
数据结构
static final class WNode {
volatile WNode prev; // 前驱
volatile WNode next; //后继
volatile WNode cowait; // list of linked readers 读线程栈
volatile Thread thread; // non-null while possibly parked 当前节点线程
volatile int status; // 0, WAITING, or CANCELLED 当前节点状态
final int mode; // RMODE or WMODE 节点模式
WNode(int m, WNode p) { mode = m; prev = p; }
}
队列的元素和aps里面的node节点是相似的, 但是很大的一个不同点的就是如果读阶段入队,发现前驱是读模式的话,当前的读节点会进入前驱节点的cowait,也就是入栈
获取锁的方法asWriteLock、asReadLock、asReadWriteLock
// views
transient ReadLockView readLockView; //读锁视图
transient WriteLockView writeLockView; //写锁视图
transient ReadWriteLockView readWriteLockView; //读写锁视图
ReadLockView和WriteLockView 都实现了Lock接口, ReadWriteLockView 则实现了ReadWriteLock。
ReadLockView和WriteLockView对于 条件队列的操作都是不支持的。
写锁加锁方法 writeLock
public long writeLock() {
long s, next; // bypass acquireWrite in fully unlocked case only
return ((((s = state) & ABITS) == 0L && //看state低8位是不是都是0, 因为写锁与读锁写锁都是互斥的
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ? //没有任何锁,cas竞争, WBIT就是一个单位的读锁
next : acquireWrite(false, 0L));//成功就返回next,失败那么就进入尝试获取锁
}
整个JUC各种条件判断都是我们很难想到的,我感觉, 这里面整个三元运算符就挺牛逼的思想。。。,
s=state 是volatile读是可以读到最新值的,与ABITS 于, 也就是看第八位的值 , WBIT就是一个读锁单位
读锁加锁 readLock
public long readLock() { //读锁是可以共享的,
long s = state, next; // bypass acquireRead on common uncontended case //< RFULL 就是说 我state上读线程的最大数量已经到达限额了
return ((whead == wtail && (s & ABITS) < RFULL && // 如果whead == wtail 就说明队列中是没有初始化或者只有一个头节点,那么我就可以尝试去获取锁, 读锁是公平的, 这也就避免了写锁饥饿的问题
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ? //没有达到state低8位的读锁数量上限,那么cas进行枪锁
next : acquireRead(false, 0L)); //抢不到 进入acquireRead进行自旋入队、自旋阻塞。
}
呃, 这里同样也是要注意state整个高56位和低8位直接,版本号,写锁,读锁这么来计数的,以及因为读锁是共享的,所以如果低8位能表示的数量不够了,需要把超出来的放入另外一个计数。
思考
StampedLock可以哪里进行优化可以使得效率高于AQS实现的RentrantReadWriteLock,
回想AQS的读写锁,模板步骤为,尝试获取锁,获取不到cas入队, 尝试获取锁,修改前驱节点,阻塞。呃,这几不不是不是入队阻塞太着急了, 假设 入队阻塞 需要10秒, 但是持有锁的线程2秒内就会释放锁,拿去阻塞是不是就效率低下了,因为阻塞时需要进入内核态,所以如果我先不阻塞,先自旋,自旋多次别人还是没有释放锁,那么我就去阻塞。
所以 StampledLock 再入队, 阻塞 这两个步骤之前都会进行多次自旋,目的在于避免阻塞,进行内核态与用户态的切换 与 自旋耗费cpu之间找一个折衷点。
acquireWrite和acquireRead 两个方法 代码多得离谱,而且很复杂。
先看有一下整体概要吧:
这个两个方法都是 分为两个大的步骤即 1、自旋入队 2、自旋阻塞
呃, 说实话真的很难啊,离谱啊。
获取写锁 acquireWrite
private long acquireWrite(boolean interruptible, long deadline) {
WNode node = null, p;
for (int spins = -1;;) { // spin while enqueuing 自旋入队, spins 自旋次数
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) //随机数大于0才算一次自旋有效
--spins; //自旋次数少1
}
else if ((p = wtail) == null) { // initialize queue 队列未初始化
WNode hd = new WNode(WMODE, null); //创建头节点, 头节点后面要跟读或者写, 所以头节点是不可以入栈的,是一个特殊的写节点,即头节点的模式为写模式
if (U.compareAndSwapObject(this, WHEAD, null, hd)) //并发创建头节点需要cas竞争
wtail = hd;
}
else if (node == null) /*初始化节点*/
node = new WNode(WMODE, p); //初始化自己的节点
else if (node.prev != p) /*设置前驱, 例子: 如果前驱节点p 取消了, 或者后面又有节点进来了, wtail已经改变了*/
node.prev = p;
else if (U.compareAndSwapObject(this, WTAIL, p, node)) { //cas修改尾指针, 这里是进行入队
p.next = node; /*入队成功把前驱执行自己*/
break; /*入队成功*/
}
}
for (int spins = -1;;) { //自旋阻塞
WNode h, np, pp; int ps;
if ((h = whead) == p) { //如果是前驱节点是头节点 比较有机会获取锁,所以进行自旋, p就是当前节点的前驱
if (spins < 0)
spins = HEAD_SPINS; //默认自旋1024
else if (spins < MAX_HEAD_SPINS) //最大自旋65536
spins <<= 1; //扩大一倍
for (int k = spins;;) { // spin at head
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) //此次自旋次数到0
break;//跳出内存循环
}
}
else if (h != null) { // help release stale waiters 帮助唤醒栈上节点 这里是一个优化点,h现在是头节点头节点前身如果是读节点,那就会有一个读栈,当前写线程帮助唤醒。
WNode c; Thread w;
while ((c = h.cowait) != null) { //头节点的栈顶元素不为空
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); //阻塞,前驱必须是-1
else if (ps == CANCELLED) { //跨过取消的前驱
if ((pp = p.prev) != null) {
node.prev = pp;
pp.next = node;
}
}
else { //行吧我阻塞
long time; // 0 argument to park means no timeout
if (deadline == 0L) //阻塞时间为0, 即阻塞时间没有限制不会自己到时间唤醒
time = 0L;
else if ((time = deadline - System.nanoTime()) <= 0L) /*阻塞时间到了*/
return cancelWaiter(node, node, false);/*取消节点*/
Thread wt = Thread.currentThread(); /*获取当前线程*/
U.putObject(wt, PARKBLOCKER, this); /*那个对象吧当前线程park了, 记录一下*/
node.thread = wt;
if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
whead == h && node.prev == p) //前驱还是-1,前驱没变, 头节点没变,队列不为空或者有人拥有了锁。 盖了帽了,这么多条件。 思考:判断头节点改变了,说明有人释放锁了,虽然投节点的后继不一定是我还是去自旋抢一下。
U.park(false, time); // emulate LockSupport.park 这里为啥敢park, 因为自己的前驱已经status为-1, 那么说明前驱释放锁的时候会unpark自己
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
if (interruptible && Thread.interrupted()) /*如果是响应中断,并且当前线程也确实是被中断唤醒park了*/
return cancelWaiter(node, node, true);
}
}
}
}
呃,整个方法,真心很长,太长了,而且很不容易读。难以理解。
读锁获取锁 acquireRead
private long acquireRead(boolean interruptible, long deadline) {
WNode node = null, p; //spins是控制 自旋次数
for (int spins = -1;;) { //自旋 抢锁 入队, 什么情况下我要入队? 1、达到规定好的自旋次数 2、写锁没有释放
WNode h;
if ((h = whead) == (p = wtail)) { // 说明队列中没有元素, 我能直接自旋是公平的
for (long m, s, ns;;) { //自旋抢锁 注意了这里如果一直没有写锁获取锁的话,是会一直自旋抢锁
if ((m = (s = state) & ABITS) < RFULL ? //判断state上面有没有到达读线程的数量限额
U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) : //没有到达就cas抢锁
(m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) //如果没有写锁,那我就进行抢锁,但是抢到锁我是把次数累加到readeroverflow
return ns; //成功获取读锁
else if (m >= WBIT) { //说明有写锁获取了锁 state上的标志位为 1000 0000 , 但是我还行要自旋,说不定写锁马上就释放了
if (spins > 0) {
if (LockSupport.nextSecondarySeed() >= 0) //随机此次自旋是否生效
--spins;
}
else {
if (spins == 0) { //打到自旋限制
WNode nh = whead, np = wtail; //重新指向头尾节点
if ((nh == h && np == p) || (h = nh) != (p = np)) //读锁自旋到spins为0, 说明写锁还没有释放,可以去入队了 这两个条件组合的意思就是队列有人进入并且队列又空了,才能继续自旋,说明别人应该会释放锁了,我还是可以公平自旋
break; //没必要自旋了 这里的break是跳出 自旋抢锁
}
spins = SPINS; //设置自旋次数 64
}
}
}
}
if (p == null) { // initialize queue 初始化队列
WNode hd = new WNode(WMODE, null); //初始化头节点,头节点默认是写模式
if (U.compareAndSwapObject(this, WHEAD, null, hd)) //多线程 cas, 没个线程都会创建一个头节点,但是头指针执行只能指向一个头节点
wtail = hd; // 设置尾指针
}
else if (node == null)
node = new WNode(RMODE, p); //初始化自己的node节点 又接着回去自旋抢锁
else if (h == p || p.mode != RMODE) { //如果头节点或者尾节点是写模式,我就可以挂都后面,如果是读模式的话就需要入栈
if (node.prev != p) //说明有人在我的前面入队了
node.prev = p; //重新入队 回去自旋
else if (U.compareAndSwapObject(this, WTAIL, p, node)) { //p尾节点是可以又多个node节点的prev指向它的,但是wtail尾节点只能指向其中的一个node,所以需要cas
p.next = node; //入队成功的 把前驱的next指向自己
break; //终于入队了 跳出去 准备自旋抢锁 阻塞。
}
}
else if (!U.compareAndSwapObject(p, WCOWAIT, //说明是读模式
node.cowait = p.cowait, node)) //快速尝试入栈
node.cowait = null; //入栈失败
else { //入栈成功 --- 自旋或者阻塞(栈上节点 )
for (;;) {
WNode pp, c; Thread w;
if ((h = whead) != null && (c = h.cowait) != null && /*说明头此时栈上还有节点*/
U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null) // help release
U.unpark(w);
if (h == (pp = p.prev) || h == p || pp == null) { //如果p的前驱是头节点 或者 p就是头节点,或者p的前驱节点位null
long m, s, ns;
do {
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s,
ns = s + RUNIT) :
(m < WBIT &&
(ns = tryIncReaderOverflow(s)) != 0L))
return ns;
} while (m < WBIT); /*写锁没有占据*/
}
if (whead == h && p.prev == pp) { /*如果头指针没有变化和pp节点都没有变化,栈上节点就该去阻塞*/
long time;
if (pp == null || h == p || p.status > 0) { /*如果p是头节点 或者 p 是取消 或者 头指针马上要指向p*/
node = null; // throw away 就不需要阻塞, 继续自旋抢锁
break;
}
if (deadline == 0L)
time = 0L;
else if ((time = deadline - System.nanoTime()) <= 0L)
return cancelWaiter(node, p, false);
Thread wt = Thread.currentThread();
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
if ((h != pp || (state & ABITS) == WBIT) &&
whead == h && p.prev == pp)
U.park(false, time);
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
if (interruptible && Thread.interrupted())
return cancelWaiter(node, p, true);
}
}
}
}
//自旋抢锁阻塞
for (int spins = -1;;) { //这时候我已经入队了,但是有没有一种可能 就是 我的前驱就已经出队了,那我前驱就是头节点,我就可以进行自旋。
WNode h, np, pp; int ps;
if ((h = whead) == p) { //p就是 我自己节点的前驱。
if (spins < 0)
spins = HEAD_SPINS; //初始自旋次数
else if (spins < MAX_HEAD_SPINS)
spins <<= 1; //扩大一倍自旋次数
for (int k = spins;;) { // spin at head
long m, s, ns;
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
(m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) { /*同样去抢锁,抢到锁了就需要出队*/
WNode c; Thread w;
whead = node; /*头节点指向我自己*/
node.prev = null; /*断开久的头节点*/
while ((c = node.cowait) != null) { /*看看我的栈里面有没有元素, 如果有元素那么 需要 全部都唤醒*/
if (U.compareAndSwapObject(node, WCOWAIT,
c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w); /*w是栈内阻塞的,所以栈内阻塞唤醒*/
}
return ns;
}
else if (m >= WBIT &&
LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
break; /*跳出自旋*/
}
}
else if (h != null) { //我前驱有节点, 协助唤醒头节点的读栈
WNode c; Thread w;
while ((c = h.cowait) != null) {
if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
}
if (whead == h) { /*说明头指针没有改变*/
if ((np = node.prev) != p) { /*判断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;
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) == WBIT) &&
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);
}
}
}
}
呃, 真的难的一批啊, 这里需要注意的是 阻塞的地方是有两个的栈里面的节点是在第一个大for循环中阻塞, 而栈顶前面的节点也就是队列里面的那个读节点是在第二个大for里面阻塞的。
呃,从代码中可以看出, 同一个读栈里面是有多个线程回去唤醒自己的cowait。
栈顶的前驱 读节点、唤醒的栈顶、获取写锁的线程、获取读锁的线程, 所以整个读栈会唤醒的越来越快,真的挺难的,而且头节点只会指向, 栈顶前驱
唤醒当前节点的后继 release
2022年8月5号 0:59 大多数人都是孤独的。
private void release(WNode h) {
if (h != null) {
WNode q; Thread w;
U.compareAndSwapInt(h, WSTATUS, WAITING, 0); //修改头节点状态为0,预备唤醒后继节点
if ((q = h.next) == null || q.status == CANCELLED) { //后继为null 或者 是取消掉了
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); //唤醒
}
}
整体的代码逻辑,呃 读倒是不难,还是那个问题,为啥需要从后面开始???
首先入队对的节点会把自己的prev指向尾节点,然后cas抢着把wtail指向自己,成功的会把wtail指向自己, 但是这个时候前尾巴节点的next还是null,接着才会把前尾节点的next指向当前wtail指向的最新尾节点。 所以存在一个空挡期, 如果我们从后往前遍历就可以避免。
写锁解锁 unstampedUnlockWrite
思考,写锁如何解锁, 首先得有写锁, 如何加上一个写单位,把低8为为1的写锁加1那就是先前进一位,这样本版号高56位就加一了,是不是很巧妙,真的很巧妙,但是要注意,高56表示的数量有,超过之后就为0,需要重置。
final void unstampedUnlockWrite() {
WNode h; long s;
if (((s = state) & WBIT) == 0L) //没有写锁 , 无法释放
throw new IllegalMonitorStateException();
state = (s += WBIT) == 0L ? ORIGIN : s; //如果state达到最大值,把state置为初始值1 0000 0000
if ((h = whead) != null && h.status != 0) //头节点不为空, 且头节点的状态为-1
release(h);
}
思考:写锁是公平锁码??这里面, 我理解是不是的,具体可以看写锁加锁的方法,首先它不会看队列里面有没有节点,直接看state没有锁就cas抢, 所以这里释放之后,虽然去唤醒了后继,但是如果这个时间点来一个写锁就会抢到当前的state,如果来一个读锁,因为读锁是会去判断队列有没有元素的,所以读锁虽然state没有锁,但是还是会去入自旋入队,自旋阻塞,直到头节点指向自己
读锁释放锁 unstampedUnlockRead
呃, 读锁是共享锁,但是state是一个共享对象,所以-1的时候是要考虑线程安全问题,需要进行cas + 循环补偿机制。
读锁释放锁需要注意 读锁是有额外的一个变量来记录溢出的数量,以及读锁是完全释放锁之后才有必要去唤醒后继。
inal void unstampedUnlockRead() {
for (;;) { //共享锁释放的时候是cas, 所以需要for
long s, m; WNode h;
if ((m = (s = state) & ABITS) == 0L || m >= WBIT) /*如果没有任何锁 或者 有写锁 这时候来释放读锁都是异常的*/
throw new IllegalMonitorStateException();
else if (m < RFULL) { //state低8位读锁数量没有溢出
if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
if (m == RUNIT && (h = whead) != null && h.status != 0) //这里为什么m == RUNIT ? 应为头节点后面的节点是读还是写 模式是不知道的, m大于1就去释放唤醒,比较亏(唤醒写锁白白唤醒),RUNIT为1, 释放读锁就没有了,需要去唤醒
release(h);
break;
}
}
else if (tryDecReaderOverflow(s) != 0L) //从溢出计数的变量里面进行减少1
break; //从溢出锁的数量里面释放了锁,跳出循环
}
}
从溢出的计数释放读锁 tryDecReaderOverflow
private long tryDecReaderOverflow(long s) {
// assert (s & ABITS) >= RFULL;
if ((s & ABITS) == RFULL) { //达到读锁数量限制
if (U.compareAndSwapLong(this, STATE, s, s | RBITS)) { //RBIT 0111 1111 与s 0111 1110 就是加一的意思
int r; long next;
if ((r = readerOverflow) > 0) {
readerOverflow = r - 1; //这里为啥能直接这样赋值,读r , r-1, 不是原子操作不得裂开。 关键点就是在于cas 修改了 state, 这样if ((s & ABITS) == RFULL) 这个条件在cas成功的线程不把state修改为next之前是进不来的, 也就线程安全了
next = s;
}
else
next = s - RUNIT; //说明还没有溢出到readerOverflow
state = next;
return next;
}
}
else if ((LockSupport.nextSecondarySeed() &
OVERFLOW_YIELD_RATE) == 0) //说明别的线程再释放读锁中,当前线程礼让一下
Thread.yield();
return 0L;
}
这个方法值得好好研究一下, 线程礼让、以及各种巧妙的判断、 计算、如果实现线程安全???
呃呃,离谱了,StampedLock 有几种锁模式???
共享锁(读锁)、独占锁(写锁-悲观写)、无锁(乐观读)
他们之间可以进行相互转化吗???
乐观读带代码看一看
获取版本号
public long tryOptimisticRead() {
long s;
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L; //判断有没有写锁拥有了锁, 没有写锁就获取当前state的版本数,如果有写锁就返回0不能进行乐观读,因为有写锁了
}
public boolean validate(long stamp) {
U.loadFence(); //加了一个读屏障。 呃 x86下是没这个读屏障的,因为x86架构没有InvalidQueue
return (stamp & SBITS) == (state & SBITS);
}
注意&SBITS 得到的是版本号和写锁计数标志, 所以当在乐观读的过程中,只要有写锁获取了锁,这次乐观读就是失败的
锁转化之变成写锁 tryConvertToWriteLock
写锁变成写锁呃,不用啥操作, 乐观读变成写锁,直接加锁就行, 读锁变成写锁,需要释放读锁在获取写锁
public long tryConvertToWriteLock(long stamp) {
long a = stamp & ABITS, m, s, next;
while (((s = state) & SBITS) == (stamp & SBITS)) { /*还没有人修改过版本号或者获取写锁*/
if ((m = s & ABITS) == 0L) { //没有任何锁,说明是乐观锁想要转化成写锁
if (a != 0L) //本事就是乐观锁
break;
if (U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) /*尝试加写锁*/
return next;
}
else if (m == WBIT) { //以及有人获取了写锁
if (a != m) //自己就是写锁想到转化为写锁
break;
return stamp;
}
else if (m == RUNIT && a != 0L) { //读锁要转化为写锁,前提就是读锁就只有当前自己一个
if (U.compareAndSwapLong(this, STATE, s,
next = s - RUNIT + WBIT)) //cas获取写锁
return next;
}
else //不符合条件直接break
break;
}
return 0L;
}
注意: 可以看见读锁转换成写锁之后,之前的读锁计数已经被减去了,所以读锁不用解锁,没有了,现在已经是写锁。
注意这个while判断条件 读锁来获取锁是不影响的
锁转化之变成读锁之 tryConvertToReadLock
public long tryConvertToReadLock(long stamp) {
long a = stamp & ABITS, m, s, next; WNode h;
while (((s = state) & SBITS) == (stamp & SBITS)) { /*还没有人修改过版本号或者获取写锁*/
if ((m = s & ABITS) == 0L) { //乐观锁转换成读锁
if (a != 0L) //如果stamp不是乐观锁
break;
else if (m < RFULL) { //说明有读锁获取锁了
if (U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))
return next;
}
else if ((next = tryIncReaderOverflow(s)) != 0L) //读锁满了
return next;
}
else if (m == WBIT) { //写锁转化成读锁
if (a != m)
break;
state = next = s + (WBIT + RUNIT); //出去写锁,就是本版号加一, 然后在加上读锁
if ((h = whead) != null && h.status != 0) /*读锁是共享的,唤醒头节点*/
release(h);
return next;
}
else if (a != 0L && a < WBIT) //本身就是读锁
return stamp;
else //转换失败
break;
}
return 0L;
}
标记巧妙的一点就是乐观锁,这个时候读锁是可以进来获取的,这个时候乐观读还是可以转化为读锁
锁转化为乐观读之tryConvertToOptimisticRead
public long tryConvertToOptimisticRead(long stamp) {
long a = stamp & ABITS, m, s, next; WNode h;
U.loadFence(); //
for (;;) {
if (((s = state) & SBITS) != (stamp & SBITS)) /*说明状态发送改变 版本号或者有人获取了写锁*/
break;
if ((m = s & ABITS) == 0L) { //没有锁
if (a != 0L)
break;
return s;
}
else if (m == WBIT) {//写锁
if (a != m)
break;
state = next = (s += WBIT) == 0L ? ORIGIN : s; //释放写锁
if ((h = whead) != null && h.status != 0)
release(h); //唤醒节点
return next;
}
else if (a == 0L || a >= WBIT) /*有人获取了锁*/
break;
else if (m < RFULL) {
if (U.compareAndSwapLong(this, STATE, s, next = s - RUNIT)) {
if (m == RUNIT && (h = whead) != null && h.status != 0) /*如果是一个读锁*/
release(h); /*读锁释放完毕,唤醒后继*/
return next & SBITS; /*得到版本号和写锁标记*/
}
}
else if ((next = tryDecReaderOverflow(s)) != 0L)/*从溢出的节点释放读锁成功*/
return next & SBITS;
}
return 0L;
}
取消等待节点cancelWaiter
思考取消等待节点,这个节点有那些写节点和读节点, 那这两个有啥区别呢?相同点就是都是存在于同步队列中, 区别点就是读节点还会存在于读栈中,所以取消节点的位置就有三种类型。队列中的写节点、队列中的读节点、读栈中的读节点, 对于栈中的节点,这个栈里的所有读节点都是属于同一个组,也就是栈顶的前驱。
private long cancelWaiter(WNode node, WNode group, boolean interrupted) {
if (node != null && group != null) { //入参判断
Thread w;
node.status = CANCELLED; //设置当前节点取消状态
// unsplice cancelled nodes from group 取消组节点
for (WNode p = group, q; (q = p.cowait) != null;) { //移除group栈上取消的节点, 因为栈上只有一个cowait单向的所以需要遍历
if (q.status == CANCELLED) {
U.compareAndSwapObject(p, WCOWAIT, q, q.cowait);/*跨过取消的节点*/
p = group; // restart 从栈顶再来
}
else
p = q;
}
if (group == node) { //如果取消的是队列里面的
for (WNode r = group.cowait; r != null; r = r.cowait) { /*那得把栈里面的都要唤醒, 因为取消了前驱q*/
if ((w = r.thread) != null) //判断node是否是阻塞的,只有阻塞的node的thread才不等于空
U.unpark(w); // wake up uncancelled co-waiters
}
for (WNode pred = node.prev; pred != null; ) { // unsplice 从队列里面移除
WNode succ, pp; // find valid successor
while ((succ = node.next) == null ||
succ.status == CANCELLED) { /*如果后继是 空 或者 取消了*/
WNode q = null; // find successor the slow way
for (WNode t = wtail; t != null && t != node; t = t.prev) /*从尾节点开始往前找,找到最近的没有取消的节点, 思考:这里为啥要从后往前找 和cas入队 wtail next的赋值有关系。*/
if (t.status != CANCELLED)
q = t; // don't link if succ cancelled 记录最近的不是取消状态的节点
if (succ == q || // ensure accurate successor //这种条件判断只能是null
U.compareAndSwapObject(node, WNEXT,
succ, succ = q)) { //跨过取消node后面取消的节点
if (succ == null && node == wtail) //node是尾节点的特殊情况
U.compareAndSwapObject(this, WTAIL, node, pred); //把wtail指向 node的前驱
break;
}
}
if (pred.next == node) // unsplice pred link
U.compareAndSwapObject(pred, WNEXT, node, succ);//跨过node
if (succ != null && (w = succ.thread) != null) {
succ.thread = null;
U.unpark(w); // wake up succ to observe new pred 唤醒后继,后继会继续判断自己的前驱。
}
if (pred.status != CANCELLED || (pp = pred.prev) == null)/*前驱不是取消获取pred是头节点*/
break;
node.prev = pp; // repeat if new pred wrong/cancelled 前驱是取消状态,node的前驱指向前驱的前驱
U.compareAndSwapObject(pp, WNEXT, pred, succ); //跨过前驱
pred = pp; //新的前驱
}
}
}
WNode h; // Possibly release first waiter 唤醒等待者
while ((h = whead) != null) {
long s; WNode q; // similar to release() but check eligibility
if ((q = h.next) == null || q.status == CANCELLED) {
for (WNode t = wtail; t != null && t != h; t = t.prev)
if (t.status <= 0) /*找到不为cancel的节点*/
q = t;
}
if (h == whead) {
if (q != null && h.status == 0 && //头节状体为初始
((s = state) & ABITS) != WBIT && // waiter is eligible 没有写锁
(s == 0L || q.mode == RMODE)) //无锁或者是读锁
release(h); //唤醒头节点的后继
break;
}
}
return (interrupted || Thread.interrupted()) ? INTERRUPTED : 0L; /*1代表中断 : 0代表非中断*/
}
取消节点干4件事情:1、把当前组的取消节点都移除 2、唤醒读线程栈的 、 3、当前取消节点找到不是取消状态的前驱和不是取消状态的后继,4、唤醒头节点的后继
总结
真的很难, 加屏障的目的为了可见性, 在验证方法和转化为乐观锁的这个两个方法中都加了读屏障,就是为了在return的时候stamp变量和s变量能够读到最新修改的, 因为是stamp和s 有可能被我们使用者变成全局变量,然后再别的线程修改了, 所以这里应该是一种比较细心高手的写法。