上一节我们说了ReentrantLock,这个锁可以很好的保证线程安全,接下来我们考虑对这个锁的优化,实际上很多业务场景都是读多写少的场景,ReentrantLock是个独占锁,所以不能很好的应对并发的读请求,ReentrantReadWriteLock应运而生,可以分离读锁和写锁,其中读锁是共享锁,写锁是独占锁,提高并发性能。
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
static final class HoldCounter {
int count = 0;
// 线程id
final long tid = getThreadId(Thread.currentThread());
}
// 这是个包着HoldCounter类的ThreadLocal子类,用来记录每个线程对应的HoldCounter
static final class ThreadLocalHoldCounter
extends ThreadLocal {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
// 存放除了第一个获取读锁线程之外的其他线程的读锁重入次数
private transient ThreadLocalHoldCounter readHolds;
// 记录最后一个获取读锁的线程的获取读锁的的重入次数
private transient HoldCounter cachedHoldCounter;
// 第一个持有读锁的线程
private transient Thread firstReader = null;
// 第一个持有读锁的线程获取读锁的可重入次数
private transient int firstReaderHoldCount;
// 写锁的当前持有线程
private transient Thread exclusiveOwnerThread;
}
类似于ReentrantLock,底层依然依靠sync实现,同时还多了两个内部类ReadLock和WriteLock,上一节中AQS维护的state是锁的重入次数,本节中的state则表示读写两种状态。除此之外的其他属性,先有个概念,我们后边用到再细说
// 默认是非公平锁
public ReentrantReadWriteLock() {
this(false);
}
// 可以设置公平和非公平锁
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
static final class NonfairSync extends Sync {
// 非公平获取写锁时直接获取不需阻塞等待
final boolean writerShouldBlock() {
return false;
}
// 非公平锁获取读锁时候,若是首节点是共享节点,不需阻塞等待
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
// 这是AQS中的方法,如果当前阻塞队列非空 且 首节点(非哨兵节点)不为空 且 首节点不是共享节点 且 首节点线程不为空 ,返回true
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
// 公平锁不管是获取写锁还是读锁都需要排队等待自己成为首节点
static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
static final int SHARED_SHIFT = 16;
// 读状态是在高16位操作,加1就是加1 0000 0000 0000 0000,即加SHARED_UNIT;减1亦然,这样方便直接在state上进行操作
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 线程最大数 65535
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 所有线程获取读锁的次数和,即读锁状态
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 返回写锁的已重入次数,即写锁状态
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
这里来解释一下ReentrantReadWriteLock中一个state怎么维持两种状态,其实很简单,state是个32位的int变量,那我们就分成两份,高16位表示读,低16位表示写,假设状态为S,读状态为S>>>16,写状态为S&0x0000FFFF。
public void lock() {
sync.acquire(1);
}
写锁是独占锁,我们这里也能看到内部调用的AQS的独占模式,acquire()中的参数1在这里代表的是重入次数.这块儿在上一节中我们说过,最后又会回来调用tryAcquire().
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 获取写状态,写锁的重入次数
int w = exclusiveCount(c);
if (c != 0) {
// c!=0且w == 0说明读锁状态不为0,说明有线程持有读锁
// c!=0且w!=0且当前线程不持有写锁说明写锁被其他线程持有
//这两种情况均直接返回false,不能获得写锁,返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 此时w!=0且当前线程持有写锁,检查获取锁后会否溢出
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 不会溢出,获得写锁并更新state
setState(c + acquires);
return true;
}
// 如果不需等待自己成为首节点
// CAS设置状态,失败的话直接返回false;成功则设置当前线程为持有锁线程,返回true
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
总结一下:
有线程持有读锁时不能获取写锁
其他线程持有写锁时,不能获取写锁
这两点算是获取写锁时的基本限制条件了,如果获取失败别忘了是要进入阻塞队列的。
public void unlock() {
sync.release(1);
}
还是一样的步骤,我们直接看tryRelease()
protected final boolean tryRelease(int releases) {
// 必须持有锁才能释放锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 这里并没有跟写锁掩码相交,因为获取锁时读锁状态肯定为0
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
其实这块儿跟ReentrantLock基本完全一样,不再复述,很简单,就是重入次数减一,如果重入次数为0了,返回true;否则返回false
此外,类似于之前,我们知道还有一些响应中断的、带超市时间的获取写锁的方式,不再一一介绍
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
可以看到读锁这里采用了共享模式的获取方法,接着调用tryAcquireShared,
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 当其他线程持有写锁,获取读锁失败,return -1,接着构造共享节点加入阻塞队列
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 此时没有线程持有写锁或本线程持有写锁,可以获取读锁
// r为持有读锁的线程
int r = sharedCount(c);
// 不需要阻等待获取而且r没有溢出且CAS设置state
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 此时没有线程持有读锁,当前线程将是第一个获取读锁的线程
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
}
// 此时有线程持有读锁
// 当前线程是这些线程中第一个获取读锁的线程,更新firstReaderHoldCount
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();
// 其他线程持有写锁时直接返回-1
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更新state,更新对应的firstreader或者readHolds或者cachedHoldCounter,CAS成功返回1,否则继续自旋
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;
}
}
}
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
老规矩,调用tryReleaseShared。
注意这里doReleaseShared 释放的是因为获取写锁而被阻塞的线程
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 如果当前线程是第一个获取读锁的,更新firstReader和firstReaderHoldCount
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;
// CAS 更新state,最后返回状态是否为0,为 0 的话就说明此时没有线程持有读锁,然后调用doReleaseShared释放一个由于获取写锁而被阻塞的线程
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
首先说个问题,有了readHolds为什么还需要firstreader和cachedHoldCounter呢?
还是一句话,性能的优化,不要当然也可以,大不了就都从readHolds中查找嘛,但是,当线程很多时,查找是需要代价的,这时候就远远不如直接通过引用来查找了。
为什么当前线程持有写锁的情况下还能继续获取读锁呢?
其实就是一个可见性的问题,当前线程获取写锁后,其他线程显然不能再获取写锁,所以此时的修改操作只能在当前线程进行,此时完全可以把本线程内的任务看成是顺序执行的,别的线程不会干扰他,自然可以获取读锁进行读取操作
为什么当前线程持有读锁的情况下不能继续获取写锁呢?
如果可以允许读锁升级为写锁,这里面就涉及一个很大的竞争问题,所有的读锁都会去竞争写锁,这样以来必然引起巨大的抢占,这是非常复杂的,因为如果竞争写锁失败,那么这些线程该如何处理?是继续还原成读锁状态,还是升级为竞争写锁状态?这一点是不好处理的,所以Java的api为了让语义更加清晰,所以只支持写锁降级为读锁,不支持读锁升级为写锁。DK8中新增的StampedLock类就可以比较优雅的完成这件事,这个到后面我们再分析。
总结一下锁的获取: