1 顾名思义
其实java很多功能,都是在原基础功能上做一层变换。读写锁就是在互斥锁Reentrantlock的逻辑基础上,对状态码的使用做了很多改进,这个改进得益于二进制在计算机中的计算方式。
一句话描述读写锁:
读读共享、读写互斥、写写互斥
2.状态码的使用
ReentrantReadWriteLock和Reentrantlock的状态码都是来自AQS的state属性,该属性定义为int型,也就是32位二进制数据。在Reentrantlock并没有对state进行特殊使用,然而在ReentrantReadWriteLock中,将state分为低16位和高16位来使用。低16位主要用于写锁的操作,高16位用于读锁的操作。
举例说明:
- 一个线程占有写锁,state的二进制为:0000 0000 0000 0000 0000 0000 0000 0001
- 一个线程占有写锁,再次重入写锁,state的二进制为:0000 0000 0000 0000 0000 0000 0000 0002
- 一个线程占有读锁,state的二进制为:0000 0000 0000 0001 0000 0000 0000 0000
- 一个线程占有读锁,再次重入读锁,state的二进制为:0000 0000 0000 0002 0000 0000 0000 0000
- 一个线程先占有写锁,再获取读锁(锁降级),state的二进制为:0000 0000 0000 0001 0000 0000 0000 0001
- …
3.直入源码
3.1 构造方法
public static void main(String[] args) {
final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
}
public ReentrantReadWriteLock() {
//使用默认的有参构造器,传入false
this(false);
}
//和Reentrantlock一样,默认是非公平锁
public ReentrantReadWriteLock(boolean fair) {
//AQS实例化
sync = fair ? new FairSync() : new NonfairSync();
//读锁实例化
readerLock = new ReadLock(this);
//写锁实例化
writerLock = new WriteLock(this);
}
3.2 读写锁对象获取
public static void main(String[] args) {
final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//必须先获取写锁对象才能具体操作
lock.writeLock();
//必须先获取读锁对象才能具体操作
lock.readLock();
}
//获取写锁对象
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
//获取读锁对象
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
3.2 内部类读锁ReadLock
3.2.1 构造器和私有属性
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
//AQS对象,可想而知,该类仅仅封装最外层的方法,实际具体实现还是走的Sync的方法
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
...
3.2.2 lock方法
1.lock()
public void lock() {
//走的内部类Sync的方法
sync.acquireShared(1);
}
2.acquireShared
public final void acquireShared(int arg) {
//尝试获取读锁
if (tryAcquireShared(arg) < 0)
//获取失败,正式开始获取读锁
doAcquireShared(arg);
}
3.tryAcquireShared(int unused)
我们需要先了解Sync一些属性的定义和state的一些计算方法:
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
// 由于读锁是操作高16位部分,所以想要让读锁状态值+1,就必须让state加2的16次幂,也就是加0000 0000 0000 0001 0000 0000 0000 0000,用代码表示2的16次幂,就是1<<16,就是左移16位。
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 写锁的可重入的最大次数,由于写锁操作低16位,那么低16位最大值不就是2的16次幂-1。也就是0000 0000 0000 0000 1111 1111 1111 1111
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 可以看出和MAX_COUNT一样的值,实际上是用来做运算的,从state中取出低16位值
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// >>>表示无符号右移16位,即取出state高16值,用于判断读锁是否占有,大于1表示所有读线程获取读锁的次数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 通过将c与EXCLUSIVE_MASK 做与运算,取出state低16位值,用于判断写锁是否占有,大于1表示某个线程写锁重入的次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}
protected final int tryAcquireShared(int unused) {
// 获取当前线程对象
Thread current = Thread.currentThread();
// 获取锁状态
int c = getState();
//如果低16位不等于0,也就是写锁被占有。且独占锁不是当前线程则返回失败,为什么还要判断这个?因为存在锁降级(已经获取写锁的线程,还没释放写锁,可以再次获取读锁)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 高16位值
int r = sharedCount(c);
// readerShouldBlock很重要,请看下面详细解读
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
if (r == 0) { // 读锁数量为0
// 设置第一个读线程为当前线程
firstReader = current;
// 首次读锁重入次数为1
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 当前线程为第一个读线程,表示第一个读锁线程重入
// 首次读锁重入次数+1
firstReaderHoldCount++;
} else { // 读锁数量不为0并且不为当前线程
// 获取计数器
HoldCounter rh = cachedHoldCounter;
// 计数器为空或者计数器的tid不为当前正在运行的线程的tid
if (rh == null || rh.tid != getThreadId(current))
// 获取当前线程对应的计数器
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0) // 计数为0
//加入到readHolds中
readHolds.set(rh);
//计数+1
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
4.readerShouldBlock(难)
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
//翻译下方法名,apparently First Queued is Exclusive:确认队列第一个节点是否独占?
//第一个节点是哪个?
//独占?不就是写锁吗?
//是不是看方法名就大概知道这个方法干嘛的?
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return
//头结点不允许为空,也就是阻塞队列存在
(h = head) != null &&
//头节点下一个节点必须存在
(s = h.next) != null &&
//下一个节点不能共享,也就是写节点嘛
!s.isShared() &&
//下一个节点的线程对象不允许为空
s.thread != null;
}
- 这个方法很重要,没有这个方法读写锁在现实使用中,会出现业务性的写死锁?why?现实业务需求中,进行读操作的次数远远大于写操作,也就是读操作比写操作频繁的多。假如某个业务线程获取到读锁,然后其他大批量读业务不间断涌入,由于读读共享,导致读锁一直被读线程占有。假如一天?一周?甚至一个月?一直不间断有读业务产生。那么些写业务还用干吗?等一天?等一周?等一个月?直到等到一个机会,没有一个线程占有读锁,才能获取到锁。
- 很显然,这会很容易导致系统写业务崩盘,那么读写锁的设计意义就失去了。
- 于是有没有解决方案?当然有,如果发现阻塞队列有写线程在排队,且排队在第一个。那么,新来的读线程统统靠边站,给我老老实实排队去,这不就解决了吗?看4、5详细描述。
- 在没有哪个线程独占写锁的情况下,对新来的读线程new Reader(注意是新来的,不是新来的会怎么样?后面会有说到)想要获取读锁的家伙(线程),在获取读锁之前做一些判断。如果阻塞队列是空的,OK你可以获取;如果阻塞队列不是空的,分两种情况。一,如果第一个节点是写节点,No你不能获取读锁,给我排队去(会阻塞)。二,如果第一个节点是读节点,OK你可以获取。
- 在没有哪个线程独占写锁的情况下,对与已经获取到读锁的线程old Reader,OK你可以再次重入读锁。
- readerShouldBlock返回的true不应该立马认为线程应该阻塞吗?当然不是,步骤5立马说了,有可能是old Reader想重入读锁。readerShouldBlock方法是无法区分是new Reader还是old Reader的。它仅仅判断阻塞队列第一个节点如果是写节点,就返回true。
- 这就是为什么最后,还要来一个fullTryAcquireShared方法,该方法就是用于区分new Reader和old Reader
5.fullTryAcquireShared
…等待更新