ReentrantReadWriteLock之读写互斥

834 阅读6分钟

ReentrantReadWriteLock之读写互斥

沉浸于现实的忙碌之中,没有时间和精力思念过去,成功也就不会太远了。
——雷音
「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

代码案例

public class ReentrantWriteReadLockDemo {
    public static void main(String[] args) {
        // 定义了一个读写锁
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        // 定义了一个读锁
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        // 定义了一个写锁
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
        new Thread(() -> {
            try {
                Thread.currentThread().setName("线程 - 1");
                readLock.lock();
                System.out.println(Thread.currentThread().getName() + "拿到了读锁");
                Thread.sleep(10 * 1000);
                readLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了读锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
        try {
            Thread.sleep(5 * 1000);

        } catch (Exception e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            try {
                Thread.currentThread().setName("线程 - 2");
                System.out.println(Thread.currentThread().getName() + "想要获取写锁");
                writeLock.lock();
                System.out.println(Thread.currentThread().getName() + "拿到了写锁");
                Thread.sleep(10 * 1000);
                writeLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了写锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
        try {
            Thread.sleep(20 * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结果输出

image.png

场景一:读写互斥

读锁获取源码分析

先看看构造函数

// 定义了一个读写锁
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

public ReentrantReadWriteLock() {
    this(false);
}
// 默认创建了一个非公平锁
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

默认创建了一个非公平锁,这点其实和ReentrantLock 相似不过这里面定义了一个读锁和一个写锁,不用想了 sync 指定又是AQS 里面的同步工具类了,先看读锁的加锁方式 readLock.lock(); 点进去看看

// 获取读锁
public void lock() {
    sync.acquireShared(1);
}

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

重要方法来了 tryAcquireShared(arg) 这个方法,进去看看这个方法里面有什么

protected final int tryAcquireShared(int unused) {
	// 获取当前线程
    Thread current = Thread.currentThread();
    // 同样也是获取AQS中的state的值,此时这个值是0,因为没有其他人来获取到锁
    int c = getState(); 
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

看看这个 exclusiveCount(c) 怎么计算的

static final int SHARED_SHIFT   = 16;
// 数字1 左移16位变成了 二进制 1 0000 0000 0000 0000 十进制 65536
// 之后再减去1 变成了 二进制 1111 1111 1111 1111 十进制 65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// c 是 0 按位 & 就是 0000 0000 0000 0000 & 1111 1111 1111 1111 所以 exclusiveCount(0) = 0 
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

所以走到 sharedCount(c) 这个里面,再看看这个是怎么计算的

static final int SHARED_SHIFT   = 16;
// 0 无符号右移 16位 还是0
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

所以此时得出的 r = 0 ,再看看后面的这个逻辑

if (!readerShouldBlock() &&  r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
    if (r == 0) {
        firstReader = current;
        firstReaderHoldCount = 1;
    } else if (firstReader == current) {
        firstReaderHoldCount++;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            cachedHoldCounter = rh = readHolds.get();
        else if (rh.count == 0)
            readHolds.set(rh);
        rh.count++;
    }
    return 1;
}

看看这是怎么更新的 compareAndSetState(c, c + SHARED_UNIT)

static final int SHARED_SHIFT   = 16;
// 1 左移 16 位 1 0000 0000 0000 0000 十进制 65536 
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);

// 其实就是更新state的值
compareAndSetState(c, c + SHARED_UNIT)

此时state的值是 int 类型的值是4个字节,一个字节8位,所以一个int就是32位,读写锁中非常聪明的一点就是读锁和写锁用了一个state的值,高16位是读锁,低16位是写锁,此时state的值是 0000 0000 0000 0001 0000 0000 0000 0000 ,所以证明了现在已经被加了一个读锁了,继续往下看

// 设置了当前的第一个读锁线程
firstReader = current;
// 数量
firstReaderHoldCount = 1;

之后直接返回了 1 ,加锁成功了

分析写锁获取,此时读锁还没有释放的场景

writeLock.lock();

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

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

乍一看,和ReentrantLock 获取锁挺像的这块,继续分析一下,先分析tryAcquire(arg) 方法,底层有些差别

protected final boolean tryAcquire(int acquires) { // acquires 是 1 
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取当前的state的值,此时state的值已经被另一个线程的读锁获取到了,修改成了65536 
    // 也就是二进制的 0000 0000 0000 0001 0000 0000 0000 0000 
    int c = getState();
    
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

再分析一下这个计算方法 exclusiveCount(c)

static final int SHARED_SHIFT   = 16;
// 数字1 左移16位变成了 二进制 1 0000 0000 0000 0000 十进制 65536
// 之后再减去1 变成了 二进制 1111 1111 1111 1111 十进制 65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// c 是 65536 按位 & 就是 65536 & 65535 也就是
// 0000 0000 0000 0001 0000 0000 0000 0000 &  0000 0000 0000 0000 1111 1111 1111 1111 = 0000 0000 0000 0000 0000 0000 0000 0000
// 所以 exclusiveCount(65536) = 0 
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

继续分析此时 w 是 0 但是 c 不是0 证明被加了读锁,因为 state 高16位 不是0

if (c != 0) {
    //  w 是 0 但是 c 不是0 证明被加了读锁 直接返回了false 
    if (w == 0 || current != getExclusiveOwnerThread())
        return false;
    if (w + exclusiveCount(acquires) > MAX_COUNT)
        throw new Error("Maximum lock count exceeded");
    // Reentrant acquire
    setState(c + acquires);
    return true;
}
if (writerShouldBlock() ||
    !compareAndSetState(c, c + acquires))
    return false;
setExclusiveOwnerThread(current);
return true;

此时就需要走 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 中的addWaiter(Node.EXCLUSIVE) 方法了,独占模式

private Node addWaiter(Node mode) {
    // 创建一个节点
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

创建了一个节点,这个节点里面设置了当前节点的线程,和模式

Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}

image.png
定义了一个pred 变量指向tail 此时这个tail变量指向的也是null, 这个tail 就是AQS 队列维护的队列中的最后一个节点,所以perd 此时也是null,直接走了enq(node)方法

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

其实这个方法后续的和ReentrantLock 都是差不多的,此时定义了一个t指针,这个指针也是指向tail的,此时因为是null的所以t也是null的,所以走 compareAndSetHead(new Node())方法,其实这个方法就是创建了一个空得头节点,之后head 指针指向了这个空的头节点,并且tail 也指向了这个空的node节点.

private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

image.png
进入下一次循环,此时t指向的tail 不是空值了,所以走向了else逻辑,将新入队的节点的前驱节点指向了t指针
image.png
尝试将tail指针指向新入队的节点,并且如果成功了的话将t指针的next指向新入队node节点
image.png
此时node入队成功了,走到了 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 方法

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 此时p是指向的head节点所以再次尝试获取锁
            // 如果成功了则将node设置成新的头节点,不过因为前一个线程此时还没有
            // 释放锁,所以此时又一次失败了
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

走到了这个方法里面 shouldParkAfterFailedAcquire(p, node)

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 第一次过来的时候此时这个waitStatus 是个null值,
    // 所以第一次将这个头节点的waitStatus的值设置成了SIGNAL,也就是-1,并且返回了false
    // 再进行一次上面的for循环,上面的流程又走一遍获取锁有一次失败,此时就是第二次到来了
    // 此时头节点的 waitStatus 不是0了是,-1了所以返回了true
    int ws = pred.waitStatus; 
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

image.png
此时设置成功返回true之后将当前节点的线程通过LockSupport.park(this); 进行挂起了。到这里读锁与写锁的场景分析完成了,此时如果读锁释放锁了呢,继续分析。

读锁释放锁源码分析

readLock.unlock();

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

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

看看这个尝试释放锁的方法 tryReleaseShared(arg),这个方法有点长,慢慢分析一下

protected final boolean tryReleaseShared(int unused) { // unused = 1 
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 其实咱们这个线程是第一个读线程加的锁,此时 firstReaderHoldCount == 1,并且
    // firstReader == current 这个也成立 firstReader = null 设置成null,
    // 如果firstReaderHoldCount 不是1的话将firstReaderHoldCount--读线程持有锁的数量减1
    // 这种情况应该是一个线程加读锁加成了多了造成的
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        // 如果不是当前线程,获取一下当前持有的读锁线程个数
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        // 读锁线程个数减少1
        --rh.count;
    }
    // 核心代码还是在这块
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }
}

上面那块代码并不是核心代码,我们分析下面这块,这块代码是核心代码

for (;;) {
    // 获取当前state的值,此时是 65536 也就是 二进制的
    // 0000 0000 0000 0001 0000 0000 0000 0000
    int c = getState();
    // 计算state的值
    // SHARED_UNIT 是(1 << SHARED_SHIFT) 也是就是 1 << 16 也就是 65536
    // 那么就是65536 -65536 = 0了
    int nextc = c - SHARED_UNIT; // 0
    if (compareAndSetState(c, nextc))
        // 返回 true
        return nextc == 0; 
}

此时释放锁成功了,看看成功之后走哪个方法  doReleaseShared()

for (;;) {
    // 定义了一个h指向了 head节点
    Node h = head;
    if (h != null && h != tail) {
        // 此时头节点是signal 是-1
        int ws = h.waitStatus;
        if (ws == Node.SIGNAL) {
            // 此时将头节点的waitStatus改成 0
            if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                continue;            // loop to recheck cases
            unparkSuccessor(h);
        }
        else if (ws == 0 &&
                 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
            continue;                // loop on failed CAS
    }
    if (h == head)                   // loop if head changed
        break;
}

image.png
此时h指向的头节点并不是空的,并且也不是tail节点,因为tail此时指向了之前入队的node节点,此时头节点是signal 是-1,此时将头节点的waitStatus改成 0,unparkSuccessor(h) 见名知意,这个就是唤醒后续节点,进去看看

private void unparkSuccessor(Node node) { // 传进来的是头节点
    int ws = node.waitStatus; // 此时 waitStatus 改成了0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // s 指向的是头节点的下一个节点
    Node s = node.next;
    // 此时s不是空值,
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 所以直接唤醒了
    if (s != null)
        LockSupport.unpark(s.thread);
}

image.png
此时s不是空值,所以直接通过  LockSupport.unpark(s.thread) 唤醒了node节点的入队的时候阻塞的线程。

唤醒后的写锁线程干什么呢

先看看线程是在哪里挂起的,线程是在下面的图里面挂起的
image.png
唤醒后继续获取锁,如果此时获取锁成功了走setHead 方法,也就是将唤醒的node节点变成头节点

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

头指针指向了node节点,node节点变成null,node的前驱节点变成null,见下图
image.png
之后p.next 也等于空了,这块是帮忙垃圾回收
image.png
此时写锁也加成功了