深入 AQS 的子类 ReentrantReadWriteLock

1.为什么要有读写锁

有了 ReentrantLock 和 Synchronized ,为啥还要有 ReentrantReadWriteLock 读写锁 ?

有些操作是读多写少的,那么还要保证线程安全的话,如果还采用上面两种互斥锁,那么效率是很低的,这种场景下就可以使用 ReentrantReadWriteLock 处理

读读不互斥, 读写,写读,写写互斥

2.读写锁的实现原理

读操作,对 AQS 中 state 属性高 16 位操作

写操作,对 AQS 中 state 属性低 16 位操作

ReentrantReadWriteLock 是重入锁

写锁重入:和 ReentranLock 基本一样,只不过取值范围变小了

读锁重入:因为同一时间可能有多个线程持有锁,那么每个线程重入多少次无法通过 state 高 16 位表示,所以每个线程内部都有一个 ThreadLocal 来记录各自的重入次数。

写锁的饥饿问题:因为读锁是共享锁,那么在非公平锁中,读锁先获取到了锁,这时候 AQS 双向链表有写锁排队,此时其他的想要获取读锁的线程就可以绕过写锁去拿去资源,就会导致写锁长时间无法获取到锁,造成写锁饥饿。

写锁降级:如果当前线程获取了写锁,当前线程还能再获取读锁; 

但是当前线程获取了读锁后,当前线程就不能再获取写锁。

public class ReentrantReadWriteLockDemo {

    static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    public static void main(String[] args) throws InterruptedException {

        try {
            writeLock.lock();
            System.out.println("获取写锁成功!");

            Thread.sleep(2000);

            readLock.lock();
            System.out.println("先获取写锁,再获取读锁!");
        } finally {
            writeLock.unlock();
        }

    }
}

3.深入ReentranReadWriteLock读写锁

3.1 深入内部类WriteLock的lock

public void lock() {
    sync.acquire(1);
}


public final void acquire(int arg) {
    // 查看当前线程是否可以获取锁资源
    if (!tryAcquire(arg) &&
        // 获取锁失败
        // 将当前线程封装位 Node 节点加入 AQS 双向链表中
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

3.1.1 tryAcquire

该方法用于查看当前线程是否可以获取锁资源

protected final boolean tryAcquire(int acquires) {

    // 获取当前线程
    Thread current = Thread.currentThread();
    int c = getState();

    // 获取 state 低 16 位
    int w = exclusiveCount(c);

    if (c != 0) {

        // 当前没有线程持有写锁,那就是持有读锁咯~ 并且不是当前线程
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;

        // 判断写锁重入范围
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        setState(c + acquires);
        return true;
    }
    // 非公平锁:直接 CAS 抢锁
    // 公平锁:先看有没有排队的
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

// writerShouldBlock 的公平锁实现

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    // false: 1.没有排队的 2.排在第一
    // true: 1.有排队的,不是第一
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

// writerShouldBlock 的非公平锁实现

final boolean writerShouldBlock() {
    return false; 
}

 acquireQueued 和 addWaiter 方法和 ReentrantLock 一样~

3.2 深入内部类WriteLock的unLock

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    // 锁是否释放干净
    if (tryRelease(arg)) {
        // 锁全部释放
        // 唤醒离头节点最近的有效节点
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {

    // 自己才能释放自己的锁,否则抛异常~
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;

    // 低 16 位 - 1 是否等于 0
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

3.3 深入内部类ReadLock的lock

public void lock() {
    sync.acquireShared(1);
}

public final void acquireShared(int arg) {
    // 竞争锁资源
    if (tryAcquireShared(arg) < 0)
        // 没拿到锁资源,去排队
        doAcquireShared(arg);
}

3.3.1 tryAcquireShared

这个方法用于记录读锁的重入的次数

读锁重入的流程

// --------------------- 1. 读锁重入的核心 --------------------------
// ReentrantReadWriteLock对ThreadLocal做了封装,ThreadLocal<HoldCounter>
// 基于HoldCounter对象存储重入次数,内部有个count属性和线程id
// 每个线程都是自己的 HoldCounter

static final class HoldCounter {
    int count = 0;
    final long tid = getThreadId(Thread.currentThread());
}
      
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

// 当前线程持有的可重入读锁的数量
private transient ThreadLocalHoldCounter readHolds;

// 记录最后一个获取锁的线程可重入次数
private transient HoldCounter cachedHoldCounter;

// 记录第一个获取锁的线程
private transient Thread firstReader = null;

// 记录第一个获取锁的线程的可重入次数
private transient int firstReaderHoldCount;

源码分析

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();

    // 有写锁占据锁资源,而且不是当前线程
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;

    // 没有线程持有写锁,或者当前线程持有写锁
    // 获取 state 的高 16 位
    int r = sharedCount(c);

    // readerShouldBlock: 判断是否需要排队等待 true:排队
    // 分为公平锁和非公平锁两种实现  
    if (!readerShouldBlock() &&

        // 是否达到读锁临界
        r < MAX_COUNT &&

        // 基于 CAS 对高 16 位 + 1
        compareAndSetState(c, c + SHARED_UNIT)) {

        // ------------ 1.第一个获取锁资源的重入记录 ---------
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        }

        // ------------ 2.第一个获取锁资源的线程 -------------
        else if (firstReader == current) {
            firstReaderHoldCount++;
        } 
        
        // ------------ 3.最后一个获取锁资源的重入记录 --------
        else {

            // 3.1 从缓存中获取最后一个拿到锁资源的线程
            HoldCounter rh = cachedHoldCounter;

            // 3.2 缓存的不是当前线程,就将当前线程作为最后一个线程设置到缓存中
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();

            // 3.3 缓存的就是当前线程,重新设置缓存对象
            // 只不过它上一次全部释放了,紧接着又来了
            // 而释放锁会清除 ThreadLocal, 避免内存泄露
            else if (rh.count == 0)
                readHolds.set(rh);

            // 重入次数 + 1
            rh.count++;
        }
        return 1;
    }

    // 如果没有拿到锁资源,尝试再次获取,逻辑跟上面基本一致
    return fullTryAcquireShared(current);
}

readerShouldBlock 的公平锁实现

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    // true:当前有排队的,并且我不是排第一
    // false:当前没有排队的或者我排第一
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

readerShouldBlock 的非公平锁实现

final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    // 只要排在双向链表中第一个节点不是写锁,就返回 false,直接获取读锁
    // 避免写锁饥饿问题
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

此处避免写锁饥饿问题映照这 2.读写锁的实现原理 上的那张图

3.3.2 fullTryAcquireShared

该方法在没有拿到锁或者 CAS 失败的时候,会尝试再次获取锁,

此处的解决内存泄露问题或许有疑问,需要理清几件事:

1.remove 的是 readHolds 里边的 HoldCounter,这是每个线程独有的

2.readHolds 在调用  get 方法之后,就会执行 ThreadLocalHoldCounter 类的 initialValue方法,才会初始化 HoldCounter 对象

所以可以大概理解为,该线程之前获取过锁,只不过后来释放了,此次获取锁失败或者 CAS 失败,需要加入到 AQS 队列,就需要先清除掉 HoldCounter,避免内存泄露.

final int fullTryAcquireShared(Thread current) {

    // 声明当前线程的锁重入次数
    HoldCounter rh = null;

    // 死循环尝试获取读锁
    for (;;) {
        int c = getState();
        // 有线程持有写锁
        if (exclusiveCount(c) != 0) {
            // 而且持有写锁的不是当前线程
            if (getExclusiveOwnerThread() != current)
                return -1;
            
        } 

        // readerShouldBlock
        // 公平锁:看一下有没有排队的,排第一才抢
        // 非公平锁:只要排在第一的不是写锁,就可以获取读锁
        else if (readerShouldBlock()) {

            // 进来了就说明没获取到锁,需要加入双向链表
            // 做一些加入链表的前置操作,处理ThreadLocal内存泄露问题
            if (firstReader == current) {

                // 第一个获取锁的线程没有使用 ThreadLocal,不用处理
            } else {

                // 只有第一次会走这个 if,为了把 rh 提出去,才做的判空
                if (rh == null) {

                    // 从缓存中拿到最后一个线程的可重入记录
                    rh = cachedHoldCounter;

                    // 如果不是当前线程,就调用它自己的ThreadLocal的get拿到重入记录 
                    if (rh == null || rh.tid != getThreadId(current)) {
                        
                        // 获取每个线程自己的 ThreadLocal
                        rh = readHolds.get();

                        // 如果重入次数为0,就可以remove了
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 此处的if一定会走,清除后就得返回-1了
                // 只不过前面将rh对象提到外面,不方便判断,就多加了个if
                if (rh.count == 0)
                    return -1;
            }
        }

        // 先做限制判断,再 CAS 抢锁
        // ------- 1.判断重入次数,是否超出阈值 ------------
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");

        // ------- 2.CAS抢锁 -----------------------------
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 此处的逻辑和 tryAcquireShared 一致
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;

                // 缓存当前线程的重入次数
                cachedHoldCounter = rh;
            }
            return 1;
        }
    }
}

3.3.3 doAcquireShared

该方法和 ReentrantLock 中的 acquireQueued 逻辑类似,

只不过抢锁成功后,还需看一下下一个要被唤醒的节点是不是共享锁,如果是就继续唤醒,以此类推,如果不是,就结束

private void doAcquireShared(int arg) {
    // 将当前线程封装Node节点,加入AQS队列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取前驱节点
            final Node p = node.predecessor();
            // 前驱是头节点就可以尝试抢一波
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 将当前节点设置为 head,并且后面是共享锁就唤醒
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 根据上一个节点判断当前节点是否可以挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 取消节点
        if (failed)
            cancelAcquire(node);
    }
}

private void setHeadAndPropagate(Node node, int propagate) {
    // 拿到head节点
    Node h = head; 
    // 将当前节点设置为head节点
    setHead(node);
    // 第一个判断更多的是在信号量有处理JDK1.5 BUG的操作。
    if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
        // 拿到当前Node的next节点
        Node s = node.next;
        // 如果next节点是共享锁,直接唤醒next节点
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

3.4 深入内部类ReadLock的unLock

public void unlock() {
    sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
    // 处理state值和可重入记录
    if (tryReleaseShared(arg)) {
        // 唤醒AQS中排队的线程
        doReleaseShared();
        return true;
    }
    return false;
}

3.4.1 tryReleaseShared

该方法处理 state 值和可重入记录

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();

    // 当前线程是第一个获取锁的线程
    if (firstReader == current) {

        // 清理缓存对象和变量
        if (firstReaderHoldCount == 1)
            firstReader = null;

        // 重入次数 -1
        else
            firstReaderHoldCount--;
    } 
    
    // 当前线程不是第一个获取锁的线程
    else {
        HoldCounter rh = cachedHoldCounter;

        // 不是最后一个获取锁的线程,就从自己的ThreadLocal中获取
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;

        // 释放锁,count至少为 1
        if (count <= 1) {
            // 避免ThreadLocal内存泄露
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        // 重入计数 -1
        --rh.count;
    }

    // 死循环保证CAS更新state成功
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

3.4.2 doReleaseShared

该方法用于唤醒 AQS 中排队的线程

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 判断双向链表是否有节点
        if (h != null && h != tail) {

            // 获取头节点状态
            int ws = h.waitStatus;

            // 头节点状态为 -1,说明后继节点需要被唤醒
            if (ws == Node.SIGNAL) {

                // 将头节点从 -1 改为 0
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;

                // 从后往前找,唤醒离头节点最近的有效节点
                unparkSuccessor(h);
            }
            // 和信号量有关,忽略
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                
        }

        // unparkSuccessor方法中如果有节点被唤醒了,
        // 会重新尝试抢锁,如果抢锁成功会重试设置head
        // 和信号量也有关
        if (h == head)
            break;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Master_hl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值