038_java.util.concurrent.locks.StampedLock

简单介绍

StampedLock是对ReentrantReadWriteLock读写锁的一种改进。ReentrantReadWriteLock中的读和写都是一种悲观锁的体现,而StampedLock的思路是乐观锁,也就是说当乐观读时假定没有其它线程修改数据,读取完成后再检查下版本号有没有变化,没有变化就读取成功了,这种模式更适用于读多写少的场景。

StampedLock有以下3种模式:

1)悲观读锁:与ReadWriteLock的读锁类似,多个线程可以同时获取悲观读锁,悲观读锁是一个共享锁。
2)乐观读锁:直接操作数据,不加任何锁。在操作数据前并没有通过CAS 设置锁的状态,仅仅通过位运算测试。如果当前没有线程持有写锁 ,则简单地返回 一个非 0 的 stamp 版本信息 ,返回0则说明有线程持有写锁。 获取该 stamp 后在具体操作数据前还需要调用validate 方法验证该 stamp 是否己经不可用
3)写锁:与ReadWriteLock的写锁类似,写锁和悲观读锁是互斥的。虽然写锁与乐观读锁不会互斥,但是在数据被更新之后,之前通过乐观读锁获得的数据已经变成了脏数据。是 一 个排它锁或者独占锁,某时只有一个线程可以获取该锁,当二个线程获取该锁后,其他请求读锁和写锁的线程必须等待 ,这类似于ReentrantReadWriteLock 的写锁。

StampedLock 的读写锁都是不可重入锁,所以在获取锁后释放锁前不应该再调用会获取锁的操作,以避免造成调用线程被阻塞。这里我们跑一个测试用例看看StampedLock是怎么用的

package com.zifang.util.zex.source;

import java.util.Date;
import java.util.HashMap;
import java.util.concurrent.locks.StampedLock;

public class StampedLockTest {
    final static HashMap<String, String> data = new HashMap<>();
    final static StampedLock lock = new StampedLock();


    public static Object write(String key, String value) {
        long stamp = lock.writeLock();
        try {
            System.out.println(new Date() + ": 抢占了写锁,开始写操作");
            return data.put(key, value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(stamp);
            System.out.println(new Date() + ": 释放了写锁");
        }
        return null;
    }

    /*
     * 对共享数据的悲观读操作
     */
    public static Object pessimisticRead(String key) {
        System.out.println(new Date() + ":  进入过写模式,只能悲观读");
        long stamp = lock.readLock();
        try {
            System.out.println(new Date() + ": 获取了读锁");
            return data.get(key);
        } finally {
            System.out.println(new Date() + ": 释放了读锁");
            lock.unlockRead(stamp);
        }
    }

    /*
     * 对共享数据的乐观读操作
     */
    public static Object optimisticRead(String key) {
        String value = null;

        long stamp = lock.tryOptimisticRead();

        if (stamp != 0) {
            System.out.println(new Date() + ":  乐观锁的印戳值获取成功");
            value = data.get(key);
        } else {
            System.out.println(new Date() + ":  乐观锁的印戳值获取失败,开始使用悲观读");
            return pessimisticRead(key);
        }

        if (!lock.validate(stamp)) {
            System.out.println(new Date() + ":  乐观读的印戳值已经过期");
            return pessimisticRead(key);
        } else {
            System.out.println(new Date() + ":  乐观读的印戳值没有过期");
            return value;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        data.put("initKey", "initValue");

        Thread t1 = new Thread(() -> {
            System.out.println(optimisticRead("initKey"));
        }, "读线程1");

        Thread t2 = new Thread(() -> {
            write("key1", "value1");
        }, "写线程1");
        
        Thread t3 = new Thread(() -> {
            System.out.println(optimisticRead("initKey"));
        }, "读线程2");

        t1.start();
        t1.join();
        t2.start();
        t3.start();
        Thread.sleep(1000);
    }
}

重要属性

// 读线程的个数占有低7位
private static final int LG_READERS = 7;
// 读线程个数每次增加的单位
private static final long RUNIT = 1L;
// 写线程个数所在的位置
private static final long WBIT  = 1L << LG_READERS;  // 128 = 1000 0000
// 读线程个数所在的位置
private static final long RBITS = WBIT - 1L;  // 127 = 111 1111
// 最大读线程个数
private static final long RFULL = RBITS - 1L;  // 126 = 111 1110
// 读线程个数和写线程个数的掩码
private static final long ABITS = RBITS | WBIT;  // 255 = 1111 1111
// 读线程个数的反位,高25位全部为1
private static final long SBITS = ~RBITS;  // -128 = 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000 0000

// state的初始值
private static final long ORIGIN = WBIT << 1;  // 256 = 1 0000 0000
// 队列的头节点
private transient volatile WNode whead;
// 队列的尾节点
private transient volatile WNode wtail;
// 存储着当前的版本号,类似于AQS的状态变量state
private transient volatile long state;


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; }
}

通过属性可以看到,这是一个类似于AQS的结构,内部同样维护着一个状态变量state和一个CLH队列。其结点定义是WNode,类似于AQS队列中的节点,可以看到它组成了一个双向链表,内部维护着阻塞的线程。

构造函数

public StampedLock() {
    state = ORIGIN;
}

构造函数很简单,将state初始化为ORIGIN即可。

重要方法

写锁

writeLock方法

public long writeLock() {
    long s, next;
    // ABITS = 255 = 1111 1111
    // WBITS = 128 = 1000 0000
    // state & ABITS如果等于0,尝试原子更新state的值加WBITS
    // 如果成功则返回更新的值,如果失败调用acquireWrite()方法
    return ((((s = state) & ABITS) == 0L &&
             U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
            next : acquireWrite(false, 0L));
}

此时state为初始状态,与ABITS与运算后的值为0,所以执行后面的CAS方法,s + WBITS的值为1 1000 0000。state的高24位存储的是版本号,低8位存储的是是否有加锁,第8位存储的是写锁,低7位存储的是读锁被获取的次数,而且如果只有第8位存储写锁的话,那么写锁只能被获取一次,也就不可能重入了。

private long acquireWrite(boolean interruptible, long deadline) {
    // node为新增节点,p为尾节点(即将成为node的前置节点)
    WNode node = null, p;
    
    // 第一次自旋——入队
    for (int spins = -1;;) { // spin while enqueuing
        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)
            // 如果自旋次数小于0,则计算自旋的次数
            // 如果当前有写锁独占且队列无元素,说明快轮到自己了
            // 就自旋就行了,如果自旋完了还没轮到自己才入队
            // 则自旋次数为SPINS常量
            // 否则自旋次数为0
            spins = (m == WBIT && wtail == whead) ? SPINS : 0;
        else if (spins > 0) {
            // 当自旋次数大于0时,当前这次自旋随机减一次自旋次数
            if (LockSupport.nextSecondarySeed() >= 0)
                --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;
        }
    }

    // 第二次自旋——阻塞并等待唤醒
    for (int spins = -1;;) {
        // h为头节点,np为新增节点的前置节点,pp为前前置节点,ps为前置节点的状态
        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;;) { // spin at head
                long s, ns;
                if (((s = state) & ABITS) == 0L) {
                    if (U.compareAndSwapLong(this, STATE, s,
                                             ns = s + WBIT)) {
                        // 尝试获取写锁成功,将node设置为新头节点并清除其前置节点(gc)
                        whead = node;
                        node.prev = null;
                        return ns;
                    }
                }
                // 随机立减自旋次数,当自旋次数减为0时跳出循环再重试
                else if (LockSupport.nextSecondarySeed() >= 0 &&
                         --k <= 0)
                    break;
            }
        }
        else if (h != null) { // help release stale waiters
            // 进入这个分支很费劲,是用于协助唤醒读节点的
            // 可以这样安排:起三个写线程,两个读线程
            // 写线程1获取锁不要释放
            // 读线程1获取锁,读线程2获取锁(会阻塞)
            // 写线程2获取锁(会阻塞)
            // 写线程1释放锁,此时会唤醒读线程1
            // 在读线程1里面先不要唤醒读线程2
            // 写线程3获取锁,此时就会走到这里来了
            WNode c; Thread w;
            // 如果头节点的cowait链表(栈)不为空,唤醒里面的所有节点
            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)
                // 如果尾节点状态为0,则更新成WAITING
                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 argument to park means no timeout
                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的线程指向当前线程
                node.thread = wt;
                if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
                    whead == h && node.prev == p)
                    // 阻塞当前线程
                    U.park(false, time);  // 等同于LockSupport.park()
                    
                // 当前节点被唤醒后,清除线程
                node.thread = null;
                U.putObject(wt, PARKBLOCKER, null);
                // 如果中断了,取消当前节点
                if (interruptible && Thread.interrupted())
                    return cancelWaiter(node, node, true);
            }
        }
    }
}

这里对acquireWrite()方法做一个总结,这个方法里面有三段自旋逻辑:
第一段自旋——入队:
(1)如果头节点等于尾节点,说明没有其它线程排队,那就多自旋一会,看能不能尝试获取到写锁;
(2)否则,自旋次数为0,直接让其入队;
第二段自旋——阻塞并等待被唤醒 + 第三段自旋——不断尝试获取写锁:
(1)第三段自旋在第二段自旋内部;
(2)如果头节点等于前置节点,那就进入第三段自旋,不断尝试获取写锁;
(3)否则,尝试唤醒头节点中等待着的读线程;
(4)最后,如果当前线程一直都没有获取到写锁,就阻塞当前线程并等待被唤醒;

unlockWrite

public void unlockWrite(long stamp) {
    WNode h;
    // 检查版本号对不对
    if (state != stamp || (stamp & WBIT) == 0L)
        throw new IllegalMonitorStateException();
    // 这行代码实际有两个作用:
    // 1. 更新版本号加1
    // 2. 释放写锁
    // stamp + WBIT实际会把state的第8位置为0,也就相当于释放了写锁
    // 同时会进1,也就是高24位整体加1了
    state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
    // 如果头节点不为空,并且状态不为0,调用release方法唤醒它的下一个节点
    if ((h = whead) != null && h.status != 0)
        release(h);
}
private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        // 将其状态改为0
        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;
        }
        // 唤醒q节点所在的线程
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

写锁的释放过程比较简单:先更改state的值,释放写锁,然后版本号加1,继续唤醒下一个等待的结点即可。

悲观读锁

public long readLock() {
    long s = state, next;  // bypass acquireRead on common uncontended case
    // 没有写锁占用,并且读锁被获取的次数未达到最大值
    // 尝试原子更新读锁被获取的次数加1
    // 如果成功直接返回,如果失败调用acquireRead()方法
    return ((whead == wtail && (s & ABITS) < RFULL &&
             U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
            next : acquireRead(false, 0L));
}

获取读锁的时候先看看现在有没有其它线程占用着写锁,如果没有的话再检测读锁被获取的次数有没有达到最大,如果没有的话直接尝试获取一次读锁,如果成功了直接返回版本号,如果没成功就调用acquireRead()排队。下面我们一起来看看acquireRead()方法:

private long acquireRead(boolean interruptible, long deadline) {
    // node为新增节点,p为尾节点
    WNode node = null, p;
    // 第一段自旋——入队
    for (int spins = -1;;) {
        // 头节点
        WNode h;
        // 如果头节点等于尾节点
        // 说明没有排队的线程了,快轮到自己了,直接自旋不断尝试获取读锁
        if ((h = whead) == (p = wtail)) {
            // 第二段自旋——不断尝试获取读锁
            for (long m, s, ns;;) {
                // 尝试获取读锁,如果成功了直接返回版本号
                if ((m = (s = state) & ABITS) < RFULL ?
                    U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
                    (m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
                    // 如果读线程个数达到了最大值,会溢出,返回的是0
                    return ns;
                else if (m >= WBIT) {
                    // m >= WBIT表示有其它线程先一步获取了写锁
                    if (spins > 0) {
                        // 随机立减自旋次数
                        if (LockSupport.nextSecondarySeed() >= 0)
                            --spins;
                    }
                    else {
                        // 如果自旋次数为0了,看看是否要跳出循环
                        if (spins == 0) {
                            WNode nh = whead, np = wtail;
                            if ((nh == h && np == p) || (h = nh) != (p = np))
                                break;
                        }
                        // 设置自旋次数
                        spins = SPINS;
                    }
                }
            }
        }
        // 如果尾节点为空,初始化头节点和尾节点
        if (p == null) { // initialize queue
            WNode hd = new WNode(WMODE, null);
            if (U.compareAndSwapObject(this, WHEAD, null, hd))
                wtail = hd;
        }
        else if (node == null)
            // 如果新增节点为空,初始化之
            node = new WNode(RMODE, p);
        else if (h == p || p.mode != RMODE) {
            // 如果头节点等于尾节点或者尾节点不是读模式
            // 当前节点入队
            if (node.prev != p)
                node.prev = p;
            else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
                p.next = node;
                break;
            }
        }
        else if (!U.compareAndSwapObject(p, WCOWAIT,
                                         node.cowait = p.cowait, node))
            // 接着上一个elseif,这里肯定是尾节点为读模式了
            // 将当前节点加入到尾节点的cowait中,这是一个栈
            // 上面的CAS成功了是不会进入到这里来的
            node.cowait = null;
        else {
            // 第三段自旋——阻塞当前线程并等待被唤醒
            for (;;) {
                WNode pp, c; Thread w;
                // 如果头节点不为空且其cowait不为空,协助唤醒其中等待的读线程
                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) {
                    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) {
                    long time;
                    // 如果前前置节点为空,或者头节点等于前置节点,或者前置节点已取消
                    // 从第一个for自旋开始重试
                    if (pp == null || h == p || p.status > 0) {
                        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中
                    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循环处,参考上面第一段自旋中有一个break,当第一个读线程入队的时候break出来的
    
    // 第五段自旋——跟上面的逻辑差不多,只不过这里单独搞一个自旋针对第一个读线程
    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;;) { // 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;
                    // 唤醒当前节点中所有等待着的读线程
                    // 因为当前节点是第一个读节点,所以它是在队列中的,其它读节点都是挂这个节点的cowait栈中的
                    while ((c = node.cowait) != null) {
                        if (U.compareAndSwapObject(node, WCOWAIT,
                                                   c, c.cowait) &&
                            (w = c.thread) != null)
                            U.unpark(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) {
                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);
            }
        }
    }
}

读锁的获取过程比较艰辛,一共有六段自旋,逻辑如下:
(1)读节点进来都是先判断是头节点如果等于尾节点,说明快轮到自己了,就不断地尝试获取读锁,如果成功了就返回;
(2)如果头节点不等于尾节点,这里就会让当前节点入队,这里入队又分成了两种;
(3)一种是首个读节点入队,它是会排队到整个队列的尾部,然后跳出第一段自旋;
(4)另一种是非第一个读节点入队,它是进入到首个读节点的cowait栈中,所以更确切地说应该是入栈;
(5)不管是入队还入栈后,都会再次检测头节点是不是等于尾节点了,如果相等,则会再次不断尝试获取读锁;
(6)如果头节点不等于尾节点,那么才会真正地阻塞当前线程并等待被唤醒;
(7)上面说的首个读节点其实是连续的读线程中的首个,如果是两个读线程中间夹了一个写线程,还是老老实实的排队。

unlockRead

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) {
            // 释放一次读锁
            if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
                // 如果读锁全部都释放了,且头节点不为空且状态不为0,唤醒它的下一个节点
                if (m == RUNIT && (h = whead) != null && h.status != 0)
                    release(h);
                break;
            }
        }
        else if (tryDecReaderOverflow(s) != 0L)
            // 读线程个数溢出检测
            break;
    }
}

private void release(WNode h) {
    if (h != null) {
        WNode q; Thread w;
        // 将其状态改为0
        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;
        }
        // 唤醒q节点所在的线程
        if (q != null && (w = q.thread) != null)
            U.unpark(w);
    }
}

读锁释放的过程就比较简单了,将state的低7位减1,当减为0的时候说明完全释放了读锁,就唤醒下一个排队的线程。

乐观读锁

tryOptimisticRead

public long tryOptimisticRead() {
    long s;
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

如果没有写锁,就返回state的高25位,这里把写所在位置一起返回了,是为了后面检测数据有没有被写过。

validate

检测乐观读版本号是否变化。

public boolean validate(long stamp) {
    // 强制加入内存屏障,刷新数据
    U.loadFence();
    return (stamp & SBITS) == (state & SBITS);
}

检测两者的版本号是否一致,与SBITS与操作保证不受读操作的影响。

总结

StampedLock也是一种读写锁,但是它不是基于AQS实现的。相较于ReentrantReadWriteLock,StampedLock 多了一种乐观读的模式,以及读锁转化为写锁的方法。StampedLock内部
存在state字段,高24位存储的是版本号,写锁的释放会增加其版本号,读锁不会,低7位存储的读锁被获取的次数,第8位存储的是写锁被获取的次数。由于第八位标记写锁是否被获取,不具备重入性质。

整体上看StampedLock中获取锁的过程使用了大量的自旋操作,对于短任务的执行会比较高效,长任务的执行会浪费大量CPU;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值