sheng的学习笔记-锁-读写锁和ReentrantReadWriteLock源码分析

读写锁概念

读写锁(Readers-Writer Lock)顾名思义是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的。总结来说,读写锁的特点是:读读不互斥、读写互斥、写写互斥

读写锁使用

在 Java 语言中,读写锁是使用 ReentrantReadWriteLock 类来实现的,其中:

  • ReentrantReadWriteLock.ReadLock 表示读锁,它提供了 lock 方法进行加锁、unlock 方法进行解锁。

  • ReentrantReadWriteLock.WriteLock 表示写锁,它提供了 lock 方法进行加锁、unlock 方法进行解锁。

它的基础使用如下代码所示:

// 创建读写锁
final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 获得读锁
final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
// 获得写锁
final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
// 读锁使用
readLock.lock();
try {
    // 业务代码...
} finally {
    readLock.unlock();
}
// 写锁使用
writeLock.lock();
try {
    // 业务代码...
} finally {
    writeLock.unlock();
}

读读不互斥

多个线程可以同时获取到读锁,称之为读读不互斥,如下代码所示:

 // 创建读写锁
final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 创建读锁
final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
Thread t1 = new Thread(() -> {
    readLock.lock();
    try {
        System.out.println("[t1]得到读锁.");
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("[t1]释放读锁.");
        readLock.unlock();
    }
});
t1.start();
Thread t2 = new Thread(() -> {
    readLock.lock();
    try {
        System.out.println("[t2]得到读锁.");
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("[t2]释放读锁.");
        readLock.unlock();
    }
});
t2.start();

以上程序执行结果如下:

读写互斥

读锁和写锁同时使用是互斥的(也就是不能同时获得),这称之为读写互斥,如下代码所示:

 // 创建读写锁
final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 创建读锁
final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
// 创建写锁
final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
// 使用读锁
Thread t1 = new Thread(() -> {
    readLock.lock();
    try {
        System.out.println("[t1]得到读锁.");
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("[t1]释放读锁.");
        readLock.unlock();
    }
});
t1.start();
// 使用写锁
Thread t2 = new Thread(() -> {
    writeLock.lock();
    try {
        System.out.println("[t2]得到写锁.");
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("[t2]释放写锁.");
        writeLock.unlock();
    }
});
t2.start();

写写互斥

多个线程同时使用写锁也是互斥的,这称之为写写互斥,如下代码所示:

 // 创建读写锁
final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 创建写锁
final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
Thread t1 = new Thread(() -> {
    writeLock.lock();
    try {
        System.out.println("[t1]得到写锁.");
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("[t1]释放写锁.");
        writeLock.unlock();
    }
});
t1.start();
 
Thread t2 = new Thread(() -> {
    writeLock.lock();
    try {
        System.out.println("[t2]得到写锁.");
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        System.out.println("[t2]释放写锁.");
        writeLock.unlock();
    }
});
t2.start();

优点分析

  1. 提高了程序执行性能:多个读锁可以同时执行,相比于普通锁在任何情况下都要排队执行来说,读写锁提高了程序的执行性能。

  1. 避免读到临时数据:读锁和写锁是互斥排队执行的,这样可以保证了读取操作不会读到写了一半的临时数据。

适用场景

读写锁适合多读少写的业务场景,此时读写锁的优势最大。

总结

读写锁是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,而写锁则是互斥锁。它的完整规则是:读读不互斥、读写互斥、写写互斥。它适用于多读的业务场景,使用它可以有效的提高程序的执行性能,也能避免读取到操作了一半的临时数据。

ReentrantReadWriteLock源码分析

ReentrantReadWriteLock的基本原理和ReentrantLock没有很大的区别,

ReentrantLock源码可以参考这个文章:

https://blog.youkuaiyun.com/coldstarry/article/details/128378721

只不过在ReentantLock的基础上拓展了两个不同类型的锁,读锁和写锁。首先可以看一下ReentrantReadWriteLock的内部结构:

内部维护了一个ReadLock和一个WriteLock,整个类的附加功能也就是通过这两个内部类实现的。

那么内部又是怎么实现这个读锁和写锁的呢。由于一个类既要维护读锁又要维护写锁,那么这两个锁的状态又是如何区分的。在ReentrantReadWriteLock对象内部维护了一个读写状态:

写锁代码分析

加锁

写锁中的加锁代码是lock()函数,又会进一步调用AQS中的acquire函数:

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

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

接着会调用tryAcquire函数,因为这个函数在AQS中是空方法,所以在ReentrantReadWriteLock.Sync中重写了这个方法,尝试获取锁。如果获取锁失败,就会进入同步队列进行排队。我们接下来看ReentrantReadWriteLock.Sync.tryAcquire方法

    protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();  //当前线程
        int c = getState();  // 同步状态值
        int w = exclusiveCount(c);  // 独占锁的重数
        if (c != 0) {  // 如果有线程当前持有锁
            // w!=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;  // 加锁成功
        }
        if (writerShouldBlock() ||    // 是否有资格去尝试获得锁
            !compareAndSetState(c, c + acquires)) // CAS更新state
            return false;
        setExclusiveOwnerThread(current);  // 更新成功则设置当前线程独占
        return true;
    }

在这个方法中,比较重要的方法是exclusiveCount,这个方法是用来判断当前持有写锁的重数。方法如下:

static final int SHARED_SHIFT   = 16;
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

可以看到,exclusiveCount方法是将state和EXCLUSIVE_MASK进行相与。而EXCLUSIVE_MASK为1左移16为然后减1,即为0X0000FFFF。两者相与之后,取得同步状态的低16位,就是写锁被获取的次数。

而sharedCount方法是将state无符号右移16位,即取同步状态的高16位,表示读锁被获取的次数。具体如下图所示:

我们再回到tryAcquire方法,写锁的加锁逻辑就是:如果当前读锁被其他线程占有,或写锁被其他线程占有,则加锁失败。否则加锁或重入锁成功。

解锁

解锁是调用unlock方法,又会进一步调用AQS中的release方法:

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

    //AQS
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

在AQS中的tryRelease方法是空方法,需要自定义的同步器进行实现,具体作用就是尝试解锁。如果解锁成功就会继续唤醒后继线程。接下来我们进入到ReentrantReadWriteLock.Sync.tryRelease方法:

    protected final boolean tryRelease(int releases) {
        if (!isHeldExclusively())   // 是否当前线程持有写锁
            throw new IllegalMonitorStateException();
        int nextc = getState() - releases;   // 减少重数
        boolean free = exclusiveCount(nextc) == 0;  // 判断写锁完全释放
        if (free)
            setExclusiveOwnerThread(null);  
        setState(nextc);    // 更新state
        return free;
    }

这个方法就比较简单,和ReentrantLock释放锁的逻辑差不多。唯一的不同是通过exclusiveCount方法来获取独占锁重入次数的方式不同。

尝试获得锁

尝试获得锁会调用tryLock方法,这个方法和lock方法的区别在于tryLock是去尝试,拿到就返回true,拿不到就返回false。而lock方法拿不到会一直等待。tryLock代码如下:

    public boolean tryLock( ) {
        return sync.tryWriteLock();
    }

tryLock又会继续调用tryWriteLock方法:

    final boolean tryWriteLock() {
        Thread current = Thread.currentThread();  // 当前线程
        int c = getState();  // 同步状态
        if (c != 0) {  // 如果当前有线程获取到锁
            int w = exclusiveCount(c);  // 独占锁重入次数
            // 如果是有线程持有读锁或者持有写锁的不是当前线程,就返回false
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            // 如果已经到了最大重入数
            if (w == MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
        }
        // CAS更新state
        if (!compareAndSetState(c, c + 1))
            return false;
        setExclusiveOwnerThread(current);  // 设置线程独占
        return true;
    }

这个方法的逻辑我们看到是和tryAcquire方法差不多,唯一的区别在于tryAcquire方法中有writerShouldBlock去判断是否有资格。

读锁代码分析

加锁

加锁调用的是lock函数,又会进一步调用AQS中的acquireShared:

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

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

在acquireShared函数中,会调用tryAcquireShared去尝试获得共享锁。如果获取失败,就会调用doAcquireShared去继续尝试获得锁。在AQS中的tryAcquireShared函数是空方法,所以ReentrantReadWriteLock.Sync进行了重写:

    protected final int tryAcquireShared(int unused) {
        Thread current = Thread.currentThread();  // 当前线程
        int c = getState();  // 获取state
        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;  // 第一个读线程持有重数为1
            } else if (firstReader == current) {  // 如果本身就是当前线程持有
                firstReaderHoldCount++;  // 更新重数
            } else {
                HoldCounter rh = cachedHoldCounter;  // 获取缓存的读线程重数
                // 如果没有设置过,或cache不是当前线程
                if (rh == null || rh.tid != getThreadId(current)) 
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                // 完全释放读锁时,会将holdCounter从ThreadLocal移除,这里重新放入
                    readHolds.set(rh);  
                rh.count++;  // 重入次数增加
            }
            return 1;  // 加锁成功
        }
        // 解决读锁重入会因为readerShouldBlock方法重入失败的问题
        return fullTryAcquireShared(current);
    }

在这个方法中,我们可以看到,当有线程持有写锁的时候,读锁肯定是无法进行加锁的。此外在这个函数中,使用到了cachedHoldCounter这个变量,用来保存最近一次加读锁线程的重数。我们来看下相关代码:

    static final class HoldCounter {
        int count = 0;
        // Use id, not reference, to avoid garbage retention
        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;

HoldCounter包含两个成员变量,分别是count和tid,用来记录读锁的重数和线程id。因为sharedCount只能反映所有读锁线程共同的重数,所以需要一个变量来存储每个线程分别持有读锁的重数。所以这里引入了readHolds这个变量,它是ThreadLocalHoldCounter,是我们之前讲过的ThreadLocal类型的,相当于每个线程都会拥有各自的HoldCounter类型变量,保存了各自的读锁加锁重数,正符合我们的要求。

而cachedHoldCounter相当于是一个缓存,用来记录最近一次加读锁线程的重数。因为每次去readHolds是需要消耗时间的,通过这个缓存可以减少一定量的时间。

我们再回到tryAcquireShared方法,他的逻辑就是:判断是否有线程持有写锁,如果有的话就加锁失败。如果没有,就去尝试获得锁。但是因为readerShouldBlock会导读线程没办法重入,所以就会进入fullTryAcquireShared去解决这个问题:

    final int fullTryAcquireShared(Thread current) {
        HoldCounter rh = null;
        for (;;) {
            int c = getState();  // 获取state
            // 如果有线程持有写锁,就加锁失败
            if (exclusiveCount(c) != 0) {
                if (getExclusiveOwnerThread() != current)
                    return -1;
            } else if (readerShouldBlock()) {  // 如果没有资格去获得锁
                // Make sure we're not acquiring read lock reentrantly
                // 如果当前线程是第一个线程,那么在tryAcquiredShared中肯定已经重入了
                if (firstReader == current) {  
                    // assert firstReaderHoldCount > 0;
                } else {  // 如果第一个读线程不是当前线程,在tryAcquireShared无法重入
                    if (rh == null) {  
                        rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current)) {
                            rh = readHolds.get();
                            if (rh.count == 0)
                                readHolds.remove();
                        }
                    }
                    // 当前线程不持有锁,就返回获取失败
                    if (rh.count == 0)
                        return -1;
                }
            }
            // 如果达到最大次数了,就重入失败
            if (sharedCount(c) == MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            // 和tryAcquireShared方法实现一样
            if (compareAndSetState(c, c + SHARED_UNIT)) {
                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; // cache for release
                }
                return 1;
            }
        }
    }

能进入到fullTryAcquireShared只有两种可能:CAS失败、非第一个读线程重入失败。可以看到fullTryAcquireShared的实现逻辑和tryAcquiredShared是基本相同的,除了没有readerShouldBlock函数。

解锁

解锁方法会调用unlock函数,又会进一步调用AQS中的releaseShared方法:

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

releaseShared中的tryReleaseShared函数就是尝试释放锁,然后唤醒后继线程。AQS中的tryReleaseShared是一个空函数,所以就会调用ReentrantReadWriteLock.Sync的tryReleaseShared函数:

    protected final boolean tryReleaseShared(int unused) {
        Thread current = Thread.currentThread();  // 当前线程
        if (firstReader == current) {   // 如果第一个读线程是当前线程
            // assert firstReaderHoldCount > 0;
            if (firstReaderHoldCount == 1)  // 读锁重入数为1
                firstReader = null;  // 释放
            else
                firstReaderHoldCount--;  // 重入数-1
        } else {   // 如果第一个读线程不是当前线程
            HoldCounter rh = cachedHoldCounter; // 获取缓存
            if (rh == null || rh.tid != getThreadId(current))
                rh = readHolds.get();
            int count = rh.count;  // 当前线程持有读锁的重数
            if (count <= 1) {  // 如果重数为1
                readHolds.remove();  // readHolds中删除
                if (count <= 0)
                    throw unmatchedUnlockException();
            }
            --rh.count;  // 读锁重数-1
        }
        for (;;) {
            int c = getState();   // 获取state
            int nextc = c - SHARED_UNIT;  // state-1
            if (compareAndSetState(c, nextc))  // CAS更新state值
                // 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;  // 如果完全释放,就返回true
        }
    }

从tryReleaseShared方法中可以看到,分为两部分:更新readHolds、更新state。因为释放锁,不仅仅会减少当前线程的读锁重数(readHolds),也要减少全局读锁重数(state)。

尝试加锁

tryLock方法和之前说的一样,只会进行一次尝试,成功就返回true,失败就返回false:

    public boolean tryLock() {
        return sync.tryReadLock();
    }

接下来进一步调用tryReadLock方法:

    final boolean tryReadLock() {
        Thread current = Thread.currentThread();  // 当前线程
        for (;;) {
            int c = getState();  // 获取state
            // 如果存在独占锁,且独占锁不是当前线程,加读锁失败
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return false;
            // 读锁的总重数
            int r = sharedCount(c);
            if (r == MAX_COUNT)  // 超过加锁最大重数
                throw new Error("Maximum lock count exceeded");
            if (compareAndSetState(c, c + SHARED_UNIT)) {  // CAS更新state
                if (r == 0) {  // 如果之前没有人持有读锁
                    firstReader = current;  // 设置第一个读线程为当前线程
                    firstReaderHoldCount = 1;  // 读锁重数为1
                } else if (firstReader == current) { // 如果当前线程就是第一个持有读锁的线程
                    firstReaderHoldCount++;  // 第一个线程读锁重数+1
                } else {
                    // 更新readHolds
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;  // 读锁持有重数+1
                }
                return true;  // 加锁成功
            }
        }
    }

这个方法的基本逻辑和tryAcquiredShared是差不多的,只是少了readerShouldBlock

参考文章

https://blog.youkuaiyun.com/sufu1065/article/details/124722777

https://blog.youkuaiyun.com/weixin_41799019/article/details/128216444

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值