ReentrantReadWriteLock

  • 读写锁在同一时刻允许多个线程访问
  • 当一个写线程访问时,所有其他线程阻塞
  • 当一个读线程访问时,其他读线程不会被阻塞
  • 读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
  • 在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量

特性

  • 支持公平锁与非公平锁
  • 可重入
  • 支持锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁

核心API

  • ReentrantReadWriteLock.ReadLock readLock(): 获取读锁
  • ReentrantReadWriteLock.WriteLock writeLock(): 获取写锁
  • int getReadLockCount():返回当前读锁被获取到的次数,与线程数无关,同一个线程获取n次 返回n
  • int getReadHoldCount():返回当前线程获取读锁的次数
  • boolean isWriteLocked():判断读锁是否被获取了
  • int getWriteHoldCount():获取当前写锁被获取的次数

原理

读写状态的设计

读写锁有两个锁,而且他们是有关系的,显然是用一个AQS,但是一个AQS只有一个state。

为了用一个state表示两种锁的状态就需要对状态拆分,int是32位,所以拆成两个部分,高16位标识读状态,低16位标识写状态。

在设置或者获取状态时需要做一些位运算

写锁的获取与释放

写锁是一个支持重进入的排它锁。若写状态为0且读状态为0,直接获取写锁;若写状态大于0,看获取同步状态的线程是不是当前线程,是则写状态+1,否则,构建节点进入同步队列,自旋阻塞。

写锁的释放与ReentrantLock的释放过程基本类似,每次释放写状态-1,当写状态为0时表示写锁已被释放,唤醒同步队列里的线程,同时前次写线程的修改对后续读写线程可见。

final boolean tryWriteLock() {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c != 0) {
        int w = exclusiveCount(c); // c与0x0000FFFF求与
        // c!=0但是低16位为0表示有人拿了读锁,有人拿了读锁是不能获取写锁的
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // MAX_COUNT是0xffff, 因为写状态是只有16位的
        if (w == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    if (!compareAndSetState(c, c + 1))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}
// 这没啥好说的
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);
    return free;
}

可以思考下为什么有人拿了读锁不能获取写锁

读锁的获取与释放

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,写状态位0时总能获取,写状态不为0,不会获取,获取成功读状态就+1。

释放的时候呢就是读状态-1,减到0就是读锁释放了。

当然这里边可能会涉及记录哪些线程获取了读锁,我也没细看,感觉没什么难度。

锁降级

锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

直接搬书上的一个例子

class Process {
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    private boolean isUpdate = false;
    private LinkedList<Integer> linkedList = new LinkedList<>();

    public void processData() {
        readLock.lock();
        if (!isUpdate) {
            // 必须先释放读书
            readLock.unlock();
            writeLock.lock();
            try {
                if (!isUpdate) {
                    // 模拟生产数据
                    for (int i = 0; i < 1000; i++) {
                        linkedList.push(i);
                    }
                    isUpdate = true;
                }
                readLock.lock(); // 1
            } finally {
                writeLock.unlock();
            }
            // 写锁降级为读锁
        }

        // 2
        try {
            while (!linkedList.isEmpty()) {
                System.out.println(Thread.currentThread().getName() + "消费数据:" + linkedList.poll());
            }
        } finally {
            readLock.unlock();
        }

    }
}

public class Demo_03_04_5_LockChange {
    public static void main(String[] args) {
        Process process = new Process();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                process.processData();
            }).start();
        }
    }
}

根据锁降级还是比较难理解的,为啥要有这个东西,书上原文

主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁, 假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

假设1不加读锁的话,线程A执行到2这个位置,写锁已经释放了,读锁也没有,所可能恰好此时有别的线程(线程B)获取到了写锁,又修改了数据,此时线程A或的数据还是之前的,这就是他没有感知到线程B对数据的更新。

碎碎念

突然想到一个面试题,读写锁中读锁之多可以被多少个线程获取?写锁呢?这个是我自己想到的题。感觉能比较好的考察对读写锁原理的理解。

results matching ""

    No results matching ""