读写锁ReentrantReadWriteLock

读写锁

一、概述

有些时候使用lock锁进行同步会影响到性能效率问题。

比如说:读写锁,读锁只是读,而不会影响到数据安全问题;而写锁是会对数据造成安全问题,所以需要加锁。

但是读读是要支持并发的;读写只能是互斥的;写写也只能是互斥的;

所以为了提高并发性能问题,有了读写锁,下面来看下读写锁。

二、读写锁结构

用final修饰的读锁和写锁,在读写锁创建出来之后,就无法被修改。

而Sync是继承了AQS框架的。

三、官方案例分析

// 官方给的使用案例
class CachedData {

    // 缓存数据
    Object data;

    // 缓存是否失效。默认无效
    volatile boolean cacheValid;

    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    // 处理数据
    void processCachedData() {

        // 获取读锁
        rwl.readLock().lock();

        if (!cacheValid) {
            // 缓存失效
            // Must release read lock before acquiring write lock

            // 但是为什么需要将读锁解开,然后再次上写锁。如果相对于同一个线程来说的话,没有必要解开读锁,然后上写锁
            // 因为升级可能会导致死锁,在读写锁源码中直接拒绝了

            // 先将读锁给释放掉,然后加上写锁
            rwl.readLock().unlock();
            // 加写锁  为了保证缓存是有效的,因为可能会在写的时候存在读的情况,如果是读的话,那么就可能会存在读取得到脏数据的情况,所以需要在上面加上读锁
            rwl.writeLock().lock();
            try {
                // Recheck state because another thread might have
                // acquired write lock and changed state before we did.
                if (!cacheValid) {
                    // TODO 如果还是失效,那么需要将缓存进行更新
                    // data = ...
                    cacheValid = true;
                }
                // Downgrade by acquiring read lock before releasing write lock
                rwl.readLock().lock();
            } finally {
                rwl.writeLock().unlock(); // Unlock write, still hold read
            }
        }

        // 缓存有效
        try {
            // TODO
            // use(data);
        } finally {
            rwl.readLock().unlock();
        }
    }
}

对于多个线程来抢夺锁来说,在某一时刻,只有一个线程能够拿到读锁或者是写锁。

如果某个线程拿到的是写锁,那么其他的线程想要读锁或者是写锁,那么就只能够阻塞等待线程释放写锁;

如果某个线程拿到的是读锁,那么其他的线程如果获取得到写锁,是没有问题的。因为读锁是支持并发的;但是如果有一个线程需要获取得到的是写锁,那么就需要等待获取得到读锁的释放掉,才可以得到写锁。

3.1、为什么要在读锁释放之后再加写锁?

看到上面的代码觉得很奇怪,为什么加上读锁判断缓存失效之后,先将读锁释放掉,然后加上写锁。

为什么获取得到了读锁之后,不能再获取得到写锁?

不能够在持有读锁情况下,再持有写锁情况。但是要是在单线程条件下,理论上来说,是可以的。但是读写锁在源码层面上就直接禁止掉了这种方式。是因为存在死锁问题。

可能存在的情况:

t1 r t2 r

需求:t1和t2在持有读锁的情况下,又都想去获取得到写锁

然后t1在读锁中想要获取写锁,那么就必须要等到写锁释放了之后才能够获取得到写锁;而t2持有了读锁的时候,t1就无法持有,需要等待;而t2想要获取得到锁,就需要等待t1将锁释放掉。结果就是造成了死锁。

所以读写锁中不允许出现在持有读锁的情况下,再来持有写锁的情况。

但是在持有写锁的情况下,是可以持有读锁的。

t1 w t2 w 写锁和写锁之间是互斥的,所以在某一个时刻,只有一个写锁成功,假如说是t1锁;那么获取得到了写锁之后,读锁就不能够被其他线程持有了。只能是本线程来持有,所以是可以的。当前线程是可以操作的。

四、读写锁的升级和降级

上面已经讲过了原理,下面来看下例子:

4.1、持有读锁在持有写锁-(升级)

public class ReadWriteLockDemoOne {
    private final static Logger logger = LoggerFactory.getLogger(ReadWriteLockDemoOne.class);
    static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    public static void main(String[] args) {

        new Thread(()->{
            reentrantReadWriteLock.readLock().lock();
            logger.info("获取得到了读锁");
            reentrantReadWriteLock.writeLock().lock();
            logger.info("在获取得到读锁的情况下来获取得到写锁");
            reentrantReadWriteLock.writeLock().unlock();
            logger.info("在读锁的情况下释放了读锁");
            reentrantReadWriteLock.readLock().unlock();
            logger.info("释放读锁");
        }).start();
    }
}

4.2、持有了写锁再持有读锁-(降级)

public class ReadWriteLockDemoTwo {
    private final static Logger logger = LoggerFactory.getLogger(ReadWriteLockDemoTwo.class);
    static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    public static void main(String[] args) {

        new Thread(()->{
            reentrantReadWriteLock.writeLock().lock();
            logger.info("获取得到了写锁");
            reentrantReadWriteLock.readLock().lock();
            logger.info("在获取得到写锁的情况下来获取得到读锁");
            reentrantReadWriteLock.readLock().unlock();
            logger.info("在获取得到写锁的情况下来释放读锁");
            reentrantReadWriteLock.writeLock().unlock();
            logger.info("释放写锁");

        }).start();
    }
}

五、读写锁源码简单分析

首先来分析下,为什么在持有读锁的情况下,不能够持有写锁。

首先看下伪代码:

            reentrantReadWriteLock.readLock().lock();
            logger.info("获取得到了读锁");
            reentrantReadWriteLock.writeLock().lock();

看下writeLock().lock()的实现:

        protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            Thread current = Thread.currentThread();
            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;
        }
posted @ 2022-10-19 10:22  写的代码很烂  阅读(93)  评论(0编辑  收藏  举报