十二. 读写锁-ReentrantReadWriteLock原理分析

前言

读写锁,即ReentrantReadWriteLock,同一时刻可以允许多个读线程获取锁,但当写线程获取锁后,读线程和其它写线程应该被阻塞。

正文

下面以一个单例缓存的例子来说明ReentrantReadWriteLock的使用。

public class Cache {

    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock rLock = rwLock.readLock();
    private final Lock wLock = rwLock.writeLock();

    private final Map<String, String> map = new HashMap<>();

    private static Cache instance = null;

    private Cache() {}

    public Cache getCacheInstance() {
        if (instance == null) {
            synchronized (Cache.class) {
                if (instance == null) {
                    instance = new Cache();
                }
            }
        }
        return instance;
    }

    public String getValueByKey(String key) {
        rLock.lock();
        try {
            return map.get(key);
        } finally {
            rLock.unlock();
        }
    }

    public void addValueByKey(String key, String value) {
        wLock.lock();
        try {
            map.put(key, value);
        } finally {
            wLock.unlock();
        }
    }

    public void clearCache() {
        wLock.lock();
        try {
            map.clear();
        } finally {
            wLock.unlock();
        }
    }

}

根据例子可知,ReentrantReadWriteLock提供了一对锁:写锁读锁,并且使用规则如下。

  • 当前线程获取读锁时,读锁是否被获取不会影响读锁的获取;
  • 当前线程获取读锁时,若写锁未被获取或者写锁被当前线程获取,则允许获取读锁,否则进入等待状态;
  • 当前线程获取写锁时,若读锁已经被获取,无论获取读锁的线程是否是当前线程,都进入等待状态;
  • 当前线程获取写锁时,若写锁已经被其它线程获取,则进入等待状态。

下面将结合源码对写锁和读锁的获取和释放进行分析。首先看一下ReentrantReadWriteLock的类图。

在这里插入图片描述

ReentrantReadWriteLock一共有五个内部类,分别为SyncFairSyncNonfairSyncWriteLockReadLock,同时可以看到,只有WriteLockReadLock实现了Lock接口,因此ReentrantReadWriteLock的写锁和读锁的获取和释放实际上是由WriteLockReadLock来完成,所以这里对ReentrantReadWriteLock的工作原理进行一个简单概括:ReentrantReadWriteLock的写锁和读锁的获取和释放分别由其内部类WriteLockReadLock来完成,而WriteLockReadLock对同步状态的操作又是依赖于ReentrantReadWriteLock实现的三个自定义同步器SyncFairSyncNonfairSync

下面继续分析写锁和读锁的同步状态的设计。通过上面的分析可以知道WriteLockReadLock依赖同一个自定义同步组件Sync,因此WriteLockReadLock对同步状态进行操作时会修改同一个state变量,即需要在同一个整型变量state上维护写锁和读锁的同步状态,而Java中整型变量一共有32位,所以ReentrantReadWriteLockstate的高16位表示读锁的同步状态,低16位表示写锁的同步状态。鉴于读写锁同步状态的设计,对读写锁同步状态的运算操作归纳如下。

  • 获取写锁同步状态: state & 0x0000FFFF
  • 获取读锁同步状态: state >>> 16
  • 写锁同步状态加一: state + 1
  • 读锁同步状态加一: state + (1 << 16)

理清楚了ReentrantReadWriteLock的组件之间的关系和读写锁同步状态的设计之后,下面开始分析写锁和读锁的获取和释放。

1. 写锁的获取

WriteLocklock()方法直接调用了AbstractQueuedSynchronizer的模板方法acquire(),在acquire()方法中会调用自定义同步器Sync重写的tryAcquire()方法,下面看一下tryAcquire()方法的实现。

protected final boolean tryAcquire(int acquires) {
	Thread current = Thread.currentThread();
	// c表示state
	int c = getState();
	// w表示写锁同步状态
	int w = exclusiveCount(c);
	if (c != 0) {
		// state不为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;
	}
	// 非公平获取锁时writerShouldBlock()返回false
	// 公平获取锁时writerShouldBlock()会调用hasQueuedPredecessors()方法
	if (writerShouldBlock() ||
		!compareAndSetState(c, c + acquires))
		return false;
	setExclusiveOwnerThread(current);
	return true;
}

上述tryAcquire()方法中,在获取写锁之前会判断读锁是否被获取以及写锁是否被其它线程获取,任意一个条件满足都不允许当前线程获取写锁。同时如果写锁和读锁均没有被获取,即state为0时,还会调用writerShouldBlock()方法来实现非公平或公平锁的语义,如果是非公平锁,writerShouldBlock()方法会返回false,此时当前线程会以CAS方式修改state,修改成功则表示获取读锁成功,如果是公平锁,writerShouldBlock()方法会调用hasQueuedPredecessors()方法来判断同步队列中是否已经有正在等待获取锁资源的线程,如果有,则当前线程需要加入同步队列,后续按照等待时间越久越优先获取锁的机制来获取写锁。

2. 写锁的释放

WriteLockunlock()方法直接调用了AbstractQueuedSynchronizer的模板方法release(),在release()方法中会调用自定义同步器Sync重写的tryRelease()方法,下面看一下tryRelease()方法的实现。

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时,才表示写锁被成功释放掉。

3. 读锁的获取

ReadLocklock()方法直接调用了AbstractQueuedSynchronizer的模板方法acquireShared(),在acquireShared()方法中会调用自定义同步器Sync重写的tryAcquireShared()方法,tryAcquireShared()方法并不完整,其最后会调用fullTryAcquireShared()方法,该方法的注释说明如下。

获取读锁同步状态的完整版本,能够实现在tryAcquireShared()方法中未能实现的CAS设置状态失败重试和读锁重入的功能。

JDK1.6ReentrantReadWriteLock提供了getReadHoldCount()方法,该方法用于获取当前线程获取读锁的次数,因为该方法的加入,导致了读锁的获取的逻辑变得更为复杂,下面将结合tryAcquireShared()fullTryAcquireShared()方法的实现,在抛开为实现getReadHoldCount()方法功能而新增的逻辑的情况下,给出读锁获取的简化实现代码。

final int fullTryAcquireShared(Thread current) {
	for (;;) {
		// c表示state
		int c = getState();
		// 如果写锁被获取并且获取写锁的线程不是当前线程,则不允许获取读锁
		if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
			return -1;
		if (sharedCount(c) == MAX_COUNT)
			throw new Error("Maximum lock count exceeded");
		// 安全的将读锁同步状态加1
		if (compareAndSetState(c, c + SHARED_UNIT))
			return 1;
	}
}

由上述可知,读锁在写锁被获取并且获取写锁的线程不是当前线程的情况下,不允许被获取,以及读锁的同步状态为所有线程获取读锁的次数之和。

4. 读锁的释放

ReadLockunlock()方法直接调用了AbstractQueuedSynchronizer的模板方法releaseShared(),在releaseShared()方法中会调用自定义同步器Sync重写的tryReleaseShared()方法,该方法同样在JDK1.6中加入了较为复杂的逻辑,下面给出其简化实现代码。

protected final boolean tryReleaseShared(int unused) {
	for (;;) {
		int c = getState();
		int nextc = c - SHARED_UNIT;
		if (compareAndSetState(c, nextc))
			return nextc == 0;
	}
}

由上述可知,只有在state为0时,即读锁和写锁均被释放的情况下tryReleaseShared()方法才会返回true,在官方的注释中给出了这样设计的原因,如下所示。

释放读锁对读线程没有影响,但是当读锁和写锁均被释放的情况下,在同步队列中等待的写线程就有可能去获取写锁。

总结

ReentrantReadWriteLock的写锁和读锁的获取和释放分别由其内部类WriteLockReadLock来完成,而WriteLockReadLock对同步状态的操作又是依赖于ReentrantReadWriteLock实现的三个自定义同步器SyncFairSyncNonfairSync

写锁读锁,使用规则如下。

  • 当前线程获取读锁时,读锁是否被获取不会影响读锁的获取;
  • 当前线程获取读锁时,若写锁未被获取或者写锁被当前线程获取,则允许获取读锁,否则进入等待状态;
  • 当前线程获取写锁时,若读锁已经被获取,无论获取读锁的线程是否是当前线程,都进入等待状态;
  • 当前线程获取写锁时,若写锁已经被其它线程获取,则进入等待状态。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

樱花祭的约定

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

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

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

打赏作者

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

抵扣说明:

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

余额充值