工作之后,我对 ReentrantReadWriteLock 有了新的理解

本文详细介绍了JavaReentrantReadWriteLock的读写锁概念、使用场景、不适合的场景,以及源码中的核心变量、构造方法和Sync类的实现。重点讲解了公平锁与非公平锁的区别,以及读锁和写锁的获取策略。
摘要由CSDN通过智能技术生成

基础概念

读锁&写锁

读锁(ReadLock)

顾名思义,读锁即对读操作加的锁,对应 ReadLock。

写锁(WriteLock)

顾名思义,写锁即对写操作加的锁,对应 WriterLock。


独占锁&共享锁

独占锁(WriteLock):
  • 一次只能被一个线程持有。
  • 当某个线程持有独占锁时,其他线程无法获取独占锁或共享锁。
  • 写操作期间不允许其他线程进行读或写操作。
共享锁(ReadLock)
  • 可以被多个线程同时持有。
  • 当没有线程持有独占锁时,多个线程可以同时获取共享锁。
  • 读操作期间不会阻塞其他线程的读操作,但会阻塞写操作。

使用场景

ReentrantReadWriteLock 适合在读操作频繁、写操作相对较少且对数据一致性要求较高的场景下使用。它允许多个线程同时进行读操作,从而提高了并发性能,同时通过独占的写锁保证了写操作的原子性和一致性。

  1. 读多写少的场景: 当并发访问中读操作远远多于写操作时,ReentrantReadWriteLock 可以提供更好的性能,因为多个线程可以同时获取读锁,提高了并发读取的效率。
  2. 数据一致性要求高的场景: 如果对共享资源的写操作需要保证原子性和一致性,可以使用 ReentrantReadWriteLock 来控制对共享资源的读写操作,确保数据的正确性。
  3. 降低锁竞争: 通过允许多个线程同时进行读操作,ReentrantReadWriteLock 在一定程度上降低了锁的竞争,提高了并发性能。

不适合的场景

  1. 写操作频繁的场景: 如果写操作非常频繁,可能会导致读锁长时间被阻塞,降低了并发读取的效率,此时可以考虑其他同步机制。
  2. 对内存开销要求严格的场景: ReentrantReadWriteLock 内部维护了多个状态变量和线程队列,可能会带来一定的内存开销,在对内存要求非常严格的场景下需要考虑内存开销的影响。

代码示例

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockExample {
    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    private static int sharedResource = 0;

    public static void main(String[] args) {
        // 使用独占锁进行写操作
        new Thread(() -> {
            writeLock.lock();
            try {
                sharedResource++;
                System.out.println("Write thread is writing the shared resource: " + sharedResource);
            } finally {
                writeLock.unlock();
            }
        }).start();

        // 使用共享锁进行读操作
        new Thread(() -> {
            readLock.lock();
            try {
                System.out.println("Read thread is reading the shared resource: " + sharedResource);
            } finally {
                readLock.unlock();
            }
        }).start();
    }
}



源码理解(JDK1.8)

UML 类图

在这里插入图片描述

核心变量

通过上述的类图可以清晰地看到,ReentrantReadWriteLock 核心变量包括 sync、readerLock 和 writerLock 三个部分,其中

  1. Sync 对象,内部类,加锁/释放等核心操作的具体实现。
  2. ReadLock,内部类,顾名思义是读锁,实际上持有 Sync 对象。
  3. WriteLock,内部类,顾名思义是写锁,实际上持有 Sync 对象。
public class ReentrantReadWriteLock  
        implements ReadWriteLock, java.io.Serializable {  
    private static final long serialVersionUID = -6992448646407690164L;  
    /** Inner class providing readlock */  
    private final ReentrantReadWriteLock.ReadLock readerLock;  
    /** Inner class providing writelock */  
    private final ReentrantReadWriteLock.WriteLock writerLock;  
    /** Performs all synchronization mechanics */  
    final Sync sync;
 }

// ReadLock 内部类
public static class ReadLock implements Lock, java.io.Serializable {  
    private static final long serialVersionUID = -5992448646407690164L;  
    private final Sync sync;
}

// WriteLock 内部类
public static class WriteLock implements Lock, java.io.Serializable {  
    private static final long serialVersionUID = -4992448646407690164L;  
    private final Sync sync;
}

构造方法

ReentrantReadWriteLock 有两个构造方法

  1. 通过参数 fair 决定初始化 sync 为公平锁 or 非公平锁,并通过 sync 初始化读写锁。
  2. 无参构造默认为非公平锁。

public ReentrantReadWriteLock() {  
    this(false);  
}

public ReentrantReadWriteLock(boolean fair) {  
    sync = fair ? new FairSync() : new NonfairSync();  
    readerLock = new ReadLock(this);  
    writerLock = new WriteLock(this);  
}

// 读锁构造
protected ReadLock(ReentrantReadWriteLock lock) {  
    sync = lock.sync;  
}

// 写锁构造
protected WriteLock(ReentrantReadWriteLock lock) {  
    sync = lock.sync;  
}

Sync 类

和 ReentrantLock 一样,ReentrantReadWriteLock 也有一个内部抽象类名为 Sync ,它也继承于 AQS,它的子类也分为公平实现和非公平实现。

核心常量
 abstract static class Sync extends AbstractQueuedSynchronizer {  
    private static final long serialVersionUID = 6317671515068378041L;  
  
    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;  
  
    /** Returns the number of shared holds represented in count  */  
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }  
    /** Returns the number of exclusive holds represented in count  */  
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }   
}
  1. SHARED_SHIFT = 16
    先来回顾一下,在 ReentrantLock 的 Sync 中直接使用 AQS 中的 state 值来存储锁的状态,因为 ReentrantLock 只有一个锁,所以一个 int 类型的值就够了。
    但是,ReentrantReadWriteLock 是区分读锁和写锁的,那么一个 int 值很难同时表征这两个语义的,因此将这个值按照低 16 位和高 16 位的方式隔成两份。
    低 16 位供写锁使用,高 16 位供读锁使用。
    了解 SHARED_SHIFT = 16 的原理后,接下来几个值就不难理解了。
  1. SHARED_UNIT
    SHARED_UNIT 的值为 65536,对应的二进制为
    0000 0000 0000 0001 0000 0000 0000 0000,因为低 16 位都是 0,所以与它进行 & 操作后保留下来的只有高 16 位,也就是读锁的数量。
  1. MAX_COUNT
    MAX_COUNT 的值为 65535,对应的二进制为
    0000 0000 0000 0000 1111 1111 1111 1111。
    表示读锁和写锁的最大持有数量分别为 65535,因为读锁和写锁平均分了 32 位。
  1. EXCLUSIVE_MASK
    EXCLUSIVE_MASK 的值也是 65535 ,对应的二进制为
    0000 0000 0000 0000 1111 1111 1111 1111,和它进行 & 操作之后就能获取低 16 位数值,即写锁的数量。
需要注意的几个方法
  1. sharedCount
    入参为 int 值 c,将 c 右移 16 位,实际上就是将高 16 位移动到了低 16 位上,原本高 16 位对应的就是读锁的数量,以此获取了读锁的数量。
/** Returns the number of shared holds represented in count  */  
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
  1. exclusiveCount
    入参为 int 值 c,将 c 和 0000 0000 0000 0000 1111 1111 1111 1111 进行与操作,实际上就是将高 16 位抹去了,只保留低 16 位,以此获取了写锁的数量。
/** Returns the number of exclusive holds represented in count  */  
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  1. readerShouldBlock 和 writerShouldBlock
这两个是 sync 类中的抽象方法,在其子类公平锁和非公平锁的具体类中会实现。
readerShouldBlock() 确定当前的读锁获取操作是否应该被阻塞,返回 true 时阻塞。
writerShouldBlock() 确定当前的写锁获取操作是否应该被阻塞,返回 true 时阻塞。
abstract boolean readerShouldBlock();  
  
abstract boolean writerShouldBlock();

关于其他核心方法在使用时再细说。


Sync 的公平锁与非公平锁实现

FairSync(公平锁实现)

在公平锁的实现中,writerShouldBlock 和 readerShouldBlock 方法的实现都会去判断线程队列中是否有其他排队线,一视同仁,也符合公平的理念。

static final class FairSync extends Sync {  
    private static final long serialVersionUID = -2274990926593161451L;  
    final boolean writerShouldBlock() {  
        return hasQueuedPredecessors();  
    }  
    final boolean readerShouldBlock() {  
        return hasQueuedPredecessors();  
    }  
}

NonfairSync(非公平锁实现)

在非公平锁的实现中,对写锁的获取操作一定不会阻塞。
对读锁的操作相对复杂,需要判断线程队列中排队的第一个节点是否在请求获取写锁,如果是的话则让返回 true,对当前线程进行阻塞,让队列中的第一个节点先获取,以此来避免写锁一直拿不到,避免写锁的饥饿状态。
而如果第一个节点是读锁的话,则返回 false,不阻塞当前线程,也就是说可能插队拿到读锁,这也符合非公平的理念。

static final class NonfairSync extends Sync {  
    private static final long serialVersionUID = -8159625535654395037L;  
    final boolean writerShouldBlock() {  
        return false; // writers can always barge  
    }  
    final boolean readerShouldBlock() {  
           
    return apparentlyFirstQueuedIsExclusive();  
    }  
}

ReadLock

lock 方法
  1. 判断写锁是否已经被获取,如果写锁已经被获取且持有线程也不是当前线程,则失败。
  2. 获取读锁数量,判断读锁是否需要被阻塞,这一步针对公平锁与非公平锁有不同实现。
  3. 公平锁中,读锁需要判断等待队列中是否有其他线程在排队,有则
  4. 非公平锁中,读锁需要判断等待队列中第一个节点是否为独占模式,是的话则需要阻塞。
  5. 如果读锁被阻塞,或超过读锁的最大值,或 cas 锁时失败了,则进入最终的拿锁流程 fullTryAcquireShared 方法中。
  6. fullTryAcquireShared 的流程类似,依旧是判断写锁是否被其他线程持有,以及判断是否需要阻塞等待,并尝试 cas 拿锁。
// 获取读锁。
// 如果当前 写锁 未被获取,则直接获取读锁,此时没有写操作也就不会有数据不一致问题。
// 如果当前 写锁 已经被获取,则阻塞,否则读的线程就可能出现数据不一致问题。
public void lock() {
	// 直接调用 AQS 中的
    sync.acquireShared(1);  
}

// acquireShared 是 AQS 中的方法,tryAcquireShared 具体在 ReentrantReadWriteLock 中实现。
public final void acquireShared(int arg) {  
    if (tryAcquireShared(arg) < 0)  
        doAcquireShared(arg);  
}

protected final int tryAcquireShared(int unused) {    
    Thread current = Thread.currentThread();  
    int c = getState();
    // exclusiveCount 获取写锁的数量,如果当前写锁已经被获取,
    // 并且获取者不是当前线程,则直接返回 -1,进入 doAcquireShared 逻辑。
    if (exclusiveCount(c) != 0 &&  
        getExclusiveOwnerThread() != current)  
        return -1; 
    // sharedCount 获取读锁数量 
    int r = sharedCount(c);

	// readerShouldBlock 上文介绍过,返回 true 表示对读锁的操作阻塞。
	// 如果读锁操作不被阻塞,并且未超过读锁的最大值,则 CAS 去拿锁
    if (!readerShouldBlock() &&  
        r < MAX_COUNT &&  
        compareAndSetState(c, c + SHARED_UNIT)) {  
        // r==0 表示当前读锁还未被任何线程获取,则给 firstReader 赋值。
        if (r == 0) {  
            firstReader = current;  
            firstReaderHoldCount = 1;
        // firstReader 已经是当前线程,则自增。  
        } 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;  
    }

	// 走到这说明要么读锁操作被阻塞了,要么就是超过最大值了,或者 cas 拿锁时失败了,
	// 此时进入到完整的拿锁流程中
    return fullTryAcquireShared(current);  
}

fullTryAcquireShared 方法

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");
        // 走到这表示写锁未获取,开始 cas 尝试拿读锁。然后就是设置 firstReader  
        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;  
        }  
    }  
}

WriteLock

lock 方法
  1. 判断写锁是否已经被获取,如果写锁已经被获取且持有线程也不是当前线程,则失败。
  2. 获取读锁数量,如果读锁已经被获取并且不是的当前线程,则失败。
  3. 如果写锁的持有者是当前线程,则去尝试拿锁。
  4. 如果当前处于无锁状态,判断写锁是否需要阻塞,这一步针对公平锁与非公平锁有不同实现。
  5. 公平锁中,写锁需要判断等待队列中是否有其他线程在排队,有则阻塞。
  6. 非公平锁中,写锁不需要判断,不阻塞。
  7. 尝试 cas 拿锁。
public void lock() {
	// 调用 sync 中 acquire 方法
    sync.acquire(1);  
}

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


// 尝试拿锁,成功返回 true,失败返回 false
protected final boolean tryAcquire(int acquires) {  
    Thread current = Thread.currentThread();  
    int c = getState();
    // w = write 写锁的数量  
    int w = exclusiveCount(c);  
    // c!=0 表示当前对象有锁,可能是写锁也可能是读锁
    if (c != 0) {  
	    // 走到这说明写锁为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;  
    }

	// 走到这说明当前对象上没有锁,则判断读锁是否被阻塞,不阻塞的话则尝试拿锁。
	// writerShouldBlock 上文讲过,在公平锁中该方法会判断等待队列中是否有其他线程排队,在非公平锁中则直接返回 false。
    if (writerShouldBlock() ||  
        !compareAndSetState(c, c + acquires))  
        return false;  
    setExclusiveOwnerThread(current);  
    return true;  
}

总结

  1. 首先,我们从整体维度了解了 ReentrantReadWriteLock 读写分离的理念以及使用场景,其核心仍然为 Sync 同步器,同样基于 AQS 的能力完成加锁与释放行为,也同样包含公平与非公平两种实现,但将读和写拆分为了 ReadLock 和 WriteLock。

  2. 这之后,我们从数据结构的细节上去理解读写分离,对于锁状态的分离实际上是通过分隔低 16 位与高 16 位来实现的,借此理解了读锁与写锁的获取与写入方式,后续在其他源码中看到类似的 & 或 >> 等操作不要慌。

  3. 最后,我们深入到 ReadLock 和 WriterLock 的源码中,通过流程总结可以看到获取锁的大流程是一致的,也都遵循着同一套原则:读读共享,涉及到写都阻塞。
    1. 如果一个线程已经持有了读锁,则其他线程的读锁请求可以进入,但是写锁请求会失败。
    2. 如果一个线程已经持有了写锁,则其他线程的读锁和写锁请求都将失败。

  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值