ReentrantReadWriteLock之写读互斥

110 阅读6分钟

ReentrantReadWriteLock之写读互斥

生活的道路一旦选定,就要勇敢地走到底,决不回头。——左拉 「这是我参与2022首次更文挑战的第4天,活动详情查看: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("线程 - 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();

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

        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(); 点进去看看

// 获取写锁
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的值是0,因为他是第一次来加锁的
    // 也就是二进制的 0000 0000 0000 0000 0000 0000 0000 0000 
    int c = getState();
    int w = exclusiveCount(c); // 0
    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 是 0 按位 & 就是 0000 0000 0000 0000 0000 0000 0000 0000 & 0000 0000 0000 0000 1111 1111 1111 1111 所以 exclusiveCount(0) = 0 
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

此时c==0 看看这个方法 writerShouldBlock(),因为走到了这里,不过返回的是false

final boolean writerShouldBlock() {
    return false; // writers can always barge
}

那么我们看看compareAndSetState(c, c + acquires)这个方法吧,看看这个c + acquires是怎么计算,c + 1,那么就是将state的值从0变成了1,之后将当前的线程设置成了独占线程

setExclusiveOwnerThread(current); 
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

返回了true此时那么就是加锁成功了

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

// 获取读锁
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的值,此时这个值是1,因为之前加了写锁
    int c = getState(); 
    if (exclusiveCount(c) != 0 && // 1
        // 此时确实 getExclusiveOwnerThread() != current 所以返回了 -1 
        getExclusiveOwnerThread() != current)
        return -1;
    .... 省略
    }

再分析一下这个计算方法 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 是 1 按位 & 就是 1 & 65535 也就是
// 0000 0000 0000 0000 0000 0000 0000 0001 &  0000 0000 0000 0000 1111 1111 1111 1111 = 0000 0000 0000 0000 0000 0000 0000 0001
// 所以 exclusiveCount(1) = 1
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

返回了-1 那么走到了下面的方法中 doAcquireShared(arg)

private void doAcquireShared(int arg) {
    // 加入了一个共享模式的节点入队,先分析一下这个
    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) {
                    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);
    }
}

先看看这个流程

  final Node node = addWaiter(Node.SHARED);

看一下这个addWaiter方法,将一个共享模式传入到了该方法中,创建了一个Node节点

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

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节点.
image.png
进入下一次循环,此时t指向的tail 不是空值了,所以走向了else逻辑,将新入队的节点的前驱节点指向了t指针,尝试将tail指针指向新入队的节点,并且如果成功了的话将t指针的next指向新入队node节点
image.png
此时node节点已经入队成功了,继续往下的逻辑,定义了一个p指针指向了当前node节点的前驱节点
image.png
因为此时p指向的是head指针所以走tryAcquireShared(arg) 这个方法,再一次尝试获取锁,不过又失败了

protected final int tryAcquireShared(int unused) {
	// 获取当前线程
    Thread current = Thread.currentThread();
    // 同样也是获取AQS中的state的值,此时这个值是1,因为之前加了写锁
    int c = getState(); 
    if (exclusiveCount(c) != 0 && // 1
        // 此时确实 getExclusiveOwnerThread() != current 所以返回了 -1 
        getExclusiveOwnerThread() != current)
        return -1;
    .... 省略
}

走到下面的方法里面

if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())
    interrupted = true;

先看看这个是否应该挂起的方法

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); 进行挂起了。到这里读锁与写锁的场景分析完成了,此时如果写锁释放锁了呢,继续分析。

写锁释放锁源码分析

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

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

protected final boolean tryRelease(int releases) { // 1
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

isHeldExclusively() 这个方法就是判断你是不是当前的线程所占有的锁,如果不是的话就会抛出异常

protected final boolean isHeldExclusively() {
    return getExclusiveOwnerThread() == Thread.currentThread();
}

如果是同一个占有锁的线程 那么重新计算一下这个值,那么继续往下走

// getState() - releases 也就是 1 - 1 =0
int nextc = getState() - releases;
// 此时 exclusiveCount(0) 也等于0 所以free就是true了,
boolean free = exclusiveCount(nextc) == 0;
if (free)
    // 将当前锁的独占线程设置位null
    setExclusiveOwnerThread(null);
// 更新state的值位0
setState(nextc);
// 返回true
return free;

因为返回的是true,所以走下面的逻辑

Node h = head;
if (h != null && h.waitStatus != 0)
    unparkSuccessor(h);
return true;

定义了一个h指针指向了head节点
image.png
此时h不是空并且waitStatus 也不是0所以走unparkSuccessor(h)方法唤醒后继的节点

private void unparkSuccessor(Node node) { // 传进来的是头节点
    // 此时头节点的 waitStatus = -1
    int ws = node.waitStatus;
    if (ws < 0)
        // 将头节点的waitStatus 改成了0
        compareAndSetWaitStatus(node, ws, 0);
	// 定义一个指针s指向下一个节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 定义一个指针t指向tail节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 因为s指针不是空值所以唤醒了之前挂起的线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

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

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

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

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 ,加锁成功了