【Java】 读写锁

321 阅读3分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

一、前言

读写锁,解决了这样的问题:既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。

在读的地方合理使用读锁,在写的地方合理使用写锁,灵活控制,可以提高程序的执行效率。

Tips:  读本身是线程安全的,加读锁,主要是为了让写锁感知到,在有人读取的时候,不要同时写入。

读写锁的获取规则

在使用读写锁时遵守下面的获取规则:

  1. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请读锁,可以申请成功。
  2. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作。
  3. 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写。

所以用一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。

总结:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)



二、使用案例

ReentrantReadWriteLockReadWriteLock 的实现类,最主要的有两个方法:

  • readLock() :获取读锁
  • writeLock() :获取写锁

简单加写锁过程,如图:

concurrent-读写锁1.png

举个栗子来应用读写锁:

public class ReadWriteLockDemo {
​
    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            false);
    private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock
            .readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock
            .writeLock();
    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }
    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> read()).start();
        new Thread(() -> read()).start();
        new Thread(() -> write()).start();
        new Thread(() -> write()).start();
    }
}

输出结果如下:

Thread-0得到读锁,正在读取
Thread-1得到读锁,正在读取
Thread-0释放读锁
Thread-1释放读锁
Thread-2得到写锁,正在写入
Thread-2释放写锁
Thread-3得到写锁,正在写入
Thread-3释放写锁



三、问题

ReentrantReadWriteLock 源码

(1)写锁是如何基于 AQSstate 变量完成加锁的?

AQS 加锁,无非就是:

  1. 判断 state,更新 state
  2. 更新 AQS 当前线程

RentrantReadWriteLock 只是在此基础上,增加了些东西。

首先了解下 RentrantReadWriteLock 中的实现 AQSSync

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    abstract static class Sync extends AbstractQueuedSynchronizer {
        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
​
        // 入参 c, 一般传入 state
        // 1. 高 16 位:表示读锁数
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        
        // 2. 低 16 位:表示写锁数
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    }
}

举个栗子,其中尝试获取锁,源码如下:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {        
    protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();
        // 获取到 AQS 中 state = 0
        int c = getState();
        // state 二进制的值,高低位来代表读锁和写锁
        // 高 16 位:读锁
        // 低 16 位:写锁
        // 获得 state 低 16 位,即写锁的状态
        int w = exclusiveCount(c);
        // c != 0, 表示有人加过锁了
        if (c != 0) {
            // 1. c != 0, w == 0 情况
            // 1.1. w == 0, 所名有人加了读锁,没有人加写锁
            // 1.2. 当前线程是否为之前加锁的线程
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            
            // 2. c != 0, w != 0 情况: 之前有人加了写锁
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
           
            // 可重入锁重点:
            // 说明,是在可重入的加写锁,那么就 state += 1 即可。
            setState(c + acquires);
            return true;
        }
        // 非公平锁:这时会去尝试加锁
        // 公平锁:此时会判断队列中有等待线程,就不加锁了
        if (writerShouldBlock() ||
            !compareAndSetState(c, c + acquires))
            return false;
        setExclusiveOwnerThread(current);
        return true;
    }
}

\