ReentrantReadWriteLock 读写锁的使用及原理

本文介绍了Java的ReentrantReadWriteLock,一种允许多个读取者或单一写入者同时访问资源的锁机制。文章详细阐述了读写锁如何用于实现缓存,以及读锁和写锁的获取与释放过程,强调了锁降级的重要性及其原理,同时解释了为何不能升级锁的原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


一. 介绍

在之前讲到的 synchronized 和 ReentrantLock 都是排他锁,也就是同一时刻只能有一个线程获取到锁。而读写锁在同一时刻允许多个线程获取锁,读写锁顾名思义,有读锁和写锁,在读锁的时候,允许多个线程同时获取,但是在一个线程获取写锁的时候,其他获取到读锁或者写锁的线程都会阻塞。

二. 读写锁实现缓存

读写锁最常用的例子就是用锁的性质来实现一个缓存,缓存的特点就是读多写少,如果使用普通的锁来实现,每次都只能有一个线程获取锁,其他的线程都会阻塞。而读写锁就能给多个进行读操作的线程进行锁的并发。

在缓存的例子中,使用了一个非线程安全的 HashMap,但是在实际获取值的时候使用了读锁和写锁保证线程安全。

public class Cache {  
  
    private static final Map<String, Object> map;  
  
    private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();  
  
    private static final Lock readLock = readWriteLock.readLock();  
    private static final Lock writeLock = readWriteLock.writeLock();  
  
    static {  
        map = new HashMap<>();  
    }  
  
    public static Object get(String key) {  
        readLock.lock();  
        try {  
            return map.get(key);  
        } finally {  
            readLock.unlock();  
        }  
    }  
  
    public static Object put(String key, Object value) {  
        writeLock.lock();  
        try {  
            return map.put(key, value);  
        } finally {  
            writeLock.unlock();  
        }  
    }  
}

三. 读锁和写锁的实现

读写锁的实现也是依靠对队列同步器的自定义实现。同时还和 ReentrantLock 一样分为公平锁和非公平锁,在尝试获取锁的时候,也会根据是否是公平锁来判断是否要在等待队列中等待。

1. 读锁

读锁是一个支持多个线程获取的锁,但是读锁是和写锁排斥的,读锁实现了队列同步器中的共享式获取同步状态的方法。因为有可能会有多个线程获取锁,所以在获取锁方法的实现上,使用了 CAS 以及循环多次尝试来成功获取锁。

public static class ReadLock implements Lock, java.io.Serializable {  
    private static final long serialVersionUID = -5992448646407690164L;  
    private final Sync sync;  
  
    protected ReadLock(ReentrantReadWriteLock lock) {  
        sync = lock.sync;  
    }  
  
    public void lock() {  
        sync.acquireShared(1);  
    }  
  
    public void lockInterruptibly() throws InterruptedException {  
        sync.acquireSharedInterruptibly(1);  
    }  
  
    public boolean tryLock() {  
        return sync.tryReadLock();  
    }  
  
    public boolean tryLock(long timeout, TimeUnit unit)  
            throws InterruptedException {  
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));  
    }  
  
    public void unlock() {  
        sync.releaseShared(1);  
    }   
}

锁的获取

因为读锁可以有多个线程获取,所以在获取锁的时候会计算读锁的数量,并且读锁有最大数量的限制为 65535。如果在获取读锁的时候碰到了有线程获取了写锁,那读锁会获取失败。如果在 tryAcquireShared() 中获取锁的时候失败了,还会在最后通过 fullTryAcquireShared() 方法不断循环再次尝试获取,直到获取完成。

protected final int tryAcquireShared(int unused) {  
    Thread current = Thread.currentThread();  
    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);  
}  
  
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        } else if (readerShouldBlock()) {
            if (firstReader == current) {
            } else {
                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");
        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;
            }
            return 1;
        }
    }
}

锁的释放

读锁因为有可能会被多个线程持有,所以锁的释放会释放多个读锁,直到所有的读锁都被释放才算结束。

protected final boolean tryReleaseShared(int unused) {  
    Thread current = Thread.currentThread();  
    if (firstReader == current) {  
        if (firstReaderHoldCount == 1)  
            firstReader = null;  
        else            
	        firstReaderHoldCount--;  
    } else {  
        HoldCounter rh = cachedHoldCounter;  
        if (rh == null || rh.tid != getThreadId(current))  
            rh = readHolds.get();  
        int count = rh.count;  
        if (count <= 1) {  
            readHolds.remove();  
            if (count <= 0)  
                throw unmatchedUnlockException();  
        }  
        --rh.count;  
    }
    for (;;) {  
        int c = getState();  
        int nextc = c - SHARED_UNIT;  
        if (compareAndSetState(c, nextc))  
            return nextc == 0;
    }  
}

2. 写锁

写锁主要实现的独占式获取同步状态的方法,同时只能有一个线程获取写锁。

public static class WriteLock implements Lock, java.io.Serializable {  
    private static final long serialVersionUID = -4992448646407690164L;  
    private final Sync sync;  
  
    protected WriteLock(ReentrantReadWriteLock lock) {  
        sync = lock.sync;  
    }  
  
    public void lock() {  
        sync.acquire(1);  
    }  
  
    public void lockInterruptibly() throws InterruptedException {  
        sync.acquireInterruptibly(1);  
    }  
  
    public boolean tryLock( ) {  
        return sync.tryWriteLock();  
    }  
  
    public boolean tryLock(long timeout, TimeUnit unit)  
            throws InterruptedException {  
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));  
    }  
  
    public void unlock() {  
        sync.release(1);  
    }   
}

锁的获取

在锁的获取最后一步 writerShouldBlock() 还会根据是否是公平锁来判断当前队列中线程的节点是否是等待时间最长的,如果了解队列同步器,就知道,里面维护了一套 FIFO 的队列,如果是公平型的写锁的获取,并且队列中还有其他先进入的线程在等待,那只能先等其他线程获取写锁。

protected final boolean tryAcquire(int acquires) {  
    Thread current = Thread.currentThread();  
    int c = getState();  
    int w = exclusiveCount(c);  
    if (c != 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))  
        return false;  
    setExclusiveOwnerThread(current);  
    return true;
}

锁的释放

写锁的释放每次都会减少写状态,如果写状态减为 0,则代表写锁已经释放。

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

四. 锁降级

锁降级指的是写锁降级为读锁,其中先持有写锁,然后在不释放写锁的情况下再获取读锁,获取到读锁以后再释放写锁,这种才是真正的降级,而不是先释放写锁再获取读锁。在这里面,锁是不允许升级的,如果进行锁的升级,有可能导致获取写锁的线程被永久阻塞。

在源码中给了一套锁降级的例子。

 class CachedData {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
	        // 这边不允许锁升级,所以需要将原来的读锁先释放
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                if (!cacheValid) {
                    data = ...
                    cacheValid = true;
                }
                // 可以从写锁降级为读锁
                rwl.readLock().lock();
            } finally {
                rwl.writeLock().unlock();
            }
        }

        try {
            use(data);
        } finally {
            rwl.readLock().unlock();
        }
    }
 }

为什么不能升级锁

在写锁的获取锁的源码中,会先判断 state 变量是否有值,也就是是否有其他线程获取了锁。如果有线程已经获取到读锁了,但是在获取写锁之前没有释放读锁,这时候这个 state 变量不为 0。

但是因为读锁在获取锁的时候没有设置当前锁定的线程,也就是 getExclusiveOwnerThread() 会获取到 null 值,这个时候会判定为写锁获取失败并加入到队列中。但是在 AQS 的源码中知道,获取独占式锁的过程只有构造节点和自旋并阻塞线程的过程,并没有通知其他等待队列中的节点的过程,这个过程只发生在 release() 方法中,那这时候就会发生一个线程永远阻塞了,其他等待中的节点也没办法通知到,整个过程就会被阻塞。

相反,如果锁降级了,在持有写锁的时候再去获取死锁,就没有这些判断。

  • tryAcquire():写锁获取过程
protected final boolean tryAcquire(int acquires) {  
    Thread current = Thread.currentThread();

	// 如果这边没有事先释放读锁,值就不是 0
    int c = getState();  
    int w = exclusiveCount(c);  
    if (c != 0) {
	    // 有其他锁并且写锁的数量是 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))  
        return false;  
    setExclusiveOwnerThread(current);  
    return true;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瞎叨叨的一天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值