StampedLock学习

存在的意义

呃,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 有可能被我们使用者变成全局变量,然后再别的线程修改了, 所以这里应该是一种比较细心高手的写法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值