神仙姐姐带你撸源码,讲原理。come on!!!!!!
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
1 读锁与写锁
那么WriteLock与ReadLock对Lock接口具体是如何实现的呢?
2 AQS
AQS(AbstractQueuedSynchronizer)抽象类定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,用大白话来说,AQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。AQS简化流程图如下:
3 Sync
AQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定,但是WriteLock与ReadLock没有直接去继承AQS。因为WriteLock与ReadLock觉得,自己还要去继承AQS实现一些两者可以公用的抽象函数,不仅麻烦,还有重复劳动。所以干脆单独提供一个对锁操作的类,由WriteLock与ReadLock持有使用,这个类叫Sync。
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; //
获取低16位的条件
// 获取读锁重入数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取写锁重入数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
我们都知道AQS中维护了一个state状态变量,正常来说,维护读锁与写锁状态需要两个变量,但是为了节约资源,使用高低位切割实现state状态变量维护两种状态,即高16位表示读状态,低16位表示写状态。
Sync中还定义了HoldCounter与ThreadLocalHoldCounter:
HoldCounter是用来记录读锁重入数的对象
ThreadLocalHoldCounter是ThreadLocal变量
static final class HoldCounter {
// 读锁重入数
int count = 0;
// 线程ID
final long tid = getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
// 线程变量初始化
return new HoldCounter();
}
}
4 公平与非公平策略
在AQS流程中,获取锁失败的线程,会被构建成节点入队到CLH队列,其他线程释放锁会唤醒CLH队列的线程重新竞争锁,如下图所示(简化流程):
非公平策略是指:非CLH队列的线程与CLH队列的线程竞争锁,大家各凭本事,不会因为你是CLH队列的线程,排了很久的队,就把锁让给你。
公平策略是指:严格按照CLH队列顺序获取锁,一定会让CLH队列线程竞争成功,如果非CLH队列线程一直占用时间片,那就一直失败,直到时间片轮到CLH队列线程为止,所以公平策略的性能会更差。
为了支持公平与非公平策略,Sync扩展了FairSync、NonfairSync子类,两个子类实现了readerShouldBlock、writerShouldBlock函数,即读锁与写锁是否阻塞。
5 ReentrantReadWriteLock全局图
6 深入细节逐个击破
6.1 ReentrantReadWriteLock的创建
// 使用默认(非公平)属性创建一个新的 ReentrantReadWriteLock。
public ReentrantReadWriteLock() {
this(false);
}
// 使用给定的公平策略创建一个新的 ReentrantReadWriteLock。
// fair – 如果此锁应使用公平排序策略,则为 true
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
// 创建读锁
readerLock = new ReadLock(this);
// 创建写锁
writerLock = new WriteLock(this);
}
ReentrantReadWriteLock默认是非公平策略,如果想用公平策略,可以直接调用有参构造器,传入true即可。但不管是创建FairSync还是NonfairSync,都会触发Sync的无参构造器,因为Sync是它们的父类(本质上它们俩都是Sync)。
Sync() {
// 当前线程持有的可重入读锁数。 仅在构造函数和 readObject 中初始化。 每当线程的读取保持计数降至 0 时删除。
readHolds = new ThreadLocalHoldCounter();
// 设置资源状态值,当前是0
setState(getState()); // ensures visibility of readHolds
}
因为Sync需要提供给ReadLock与WriteLock使用,所以创建ReadLock与WriteLock时,会接收ReentrantReadWriteLock对象作为入参。
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
最后通过ReentrantReadWriteLock.sync把Sync交给了ReadLock与WriteLock。
6.2 获取写锁
我们遵守ReadWriteLock接口规范,调用ReentrantReadWriteLock.writeLock函数获取写锁对象。
public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
// 获取写锁对象
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
// 获取读锁对象
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
// ...
获取到写锁对象后,遵守Lock接口规范,调用lock函数获取写锁。WriteLock.lock函数是由Sync实现的(FairSync或NonfairSync)。
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);
}
// ...
sync.acquire(1)函数是AQS中的独占式获取锁流程模板(Sync继承自AQS):
// 独占式获取锁流程模板
public final void acquire(int arg) {
// 获独占式锁,Sync重写tryAcquire函数
if (!tryAcquire(arg) && // 获取独占锁失败
// 自旋阻塞等待获取资源,并返回线程是否被中断过标记
acquireQueued(addWaiter(Node.EXCLUSIVE)// 创建独占式标记节点 , arg))
selfInterrupt(); // 如果线程被中断过,指向线程中断操作
}
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
Thread current = Thread.currentThread();
// 获取state状态
int c = getState();
// 计算写锁数量
int w = exclusiveCount(c);
if (c != 0) {
// c != 0 && w ==0 表示存在读锁
// or
// 当前线程不是已经获取读锁的线程
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-公平与非公平实现不一样
//写锁是否阻塞 or cas同步状态失败返回false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 设置当前线程
setExclusiveOwnerThread(current);
return true;
}
流程图如下所示:
通过流程图,我们发现了一些要点:
读写互斥
写写互斥
写锁支持同一个线程重入
writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
6.3 释放写锁
获取到写锁,临界区执行完,要记得释放写锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的读写操作,调用unlock函数释放写锁(Lock接口规范)。WriteLock.unlock函数也是由Sync实现的(FairSync或NonfairSync)。
sync.release(1)执行的是AQS中的独占式释放锁流程模板(Sync继承自AQS)。
tryRelease函数代码如下:
因为同一个线程可以对相同的写锁重入多次,所以也要释放的相同的次数。
6.4 获取读锁
我们遵守ReadWriteLock接口规范,调用ReentrantReadWriteLock.readLock函数获取读锁对象。
获取到读锁对象后,遵守Lock接口规范,调用lock函数获取读锁。ReadLock.lock函数是由Sync实现的(FairSync或NonfairSync)。
sync.acquireShared(1)函数执行的是AQS中的共享式获取锁流程模板(Sync继承自AQS)。
我们只关注tryAcquireShared函数,doAcquireShared函数是AQS的获取共享式锁失败后的流程内容,不属于本文范畴,tryAcquireShared函数代码如下:
通过流程图,我们发现了一些要点:
读锁共享,读读不互斥
读锁可重入,每个获取读锁的线程都会记录对应的重入数
读写互斥,锁降级场景除外
支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
6.5 释放读锁
获取到读锁,执行完临界区后,要记得释放读锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的写操作,通过调用unlock函数释放读锁(Lock接口规范)。ReadLock.unlock函数也是由Sync实现的(FairSync或NonfairSync)。
sync.releaseShared(1)函数执行的是AQS中的共享式释放锁流程模板(Sync继承自AQS)。
我们只关注tryReleaseShared函数,doReleaseShared函数是AQS的释放共享式锁成功后的流程内容,不属于本文范畴,tryReleaseShared函数代码如下:
这里有三点需要注意:
第一点:线程读锁的重入数与读锁数量是两个概念,线程读锁的重入数是每个线程获取同一个读锁的次数,读锁数量则是所有线程的读锁重入数总和。
第二点:AQS的共享式释放锁流程模板中,只有全部的读锁被释放了,才会去执行doReleaseShared函数
第三点:因为使用的是AQS共享式流程模板,如果CLH队列后面的线程节点都是因写锁阻塞的读锁线程节点,会传播唤醒
ReentrantReadWriteLock底层实现与ReentrantLock思路一致,它们都离不开AQS,都是声明一个继承AQS的Sync,并在Sync下扩展公平与非公平策略,后续的锁相关操作都委托给公平与非公平策略执行。
我们还发现,在AQS中除了独占式模板,还有共享式模板,它们在多线程访问共享资源的流程会有所差异,就如ReentrantReadWriteLock中读锁使用共享式,写锁使用独占式。
读读不阻塞
写锁阻塞写之后的读写锁,但是不阻塞写锁之前的读锁线程
写锁会被写之前的读写锁阻塞
读锁节点唤醒会无条件传播唤醒CLH队列后面的读锁节点
写锁可以降级为读锁,防止更新丢失
读锁、写锁都支持重入