目录
一、前言
1、读写锁
2、可重入锁:
3、公平锁和非公平锁
二、接口
三、实现分析
3.1 读写状态的设计
3.1.1读位运算
3.1.2写位运算
3.2 写锁的获取与释放
3.2.1 tryAcquire方法
3.2.2 tryRelease方法
3.3 读锁的获取与释放
4、公平锁和非公平锁
4.1 使用原理
4.2 公平锁
4.3 非公平锁
ReentrantReadWriteLock中包含三种锁:读写锁、可重入锁、公平/非公平锁
又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
ReadWriteLock仅定义了获取读锁和写锁的两个方法,即ReadLock()方法和writeLock()方法,而其实现:ReentranReadWriteLock,除接口方法之外,还提供了一些便于外界监控其内部工作状态的方法:
特性 | 说明 |
公平性选择 | 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平 |
重进入 | 该锁支持重进入。以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁;而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁。 |
锁降级 | 遵循获取写锁、获取读锁再获取写锁的次序,写锁可以降级成为读锁 |
分析ReentranReadWriteLock的实现,主要包括读写状态的设计、写锁的获取与释放、读锁的获取与释放以及锁降级。
ReentranReadWriteLock中使用一个int state变量来维护读锁和写锁的状态。int类型为4字节32位,用state的高16位来存储读锁的状态,用低16位来存储写锁的状态。
位运算代码
//偏移位数
static final int SHARED_SHIFT = 16;
//读锁计数基本单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//读锁、写锁可重入最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//获取低16位的条件
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; }
因为ReentranReadWriteLock中的int
整型变量state
要同时维护读锁、写锁两种状态,所以ReentranReadWriteLock的是通过高低位切割来实现。
int占4个字节,一个字节8个比特位,一共2位,切割一下,高16位表示读,底16位表示写。
读位运算
//偏移位数
static final int SHARED_SHIFT = 16;
//读锁计数基本单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
读锁使用高16
位,每次获取读锁成功+1
,所以读锁计数基本单位是1
的高16
位,即1
左移16
位(1 << 16
)。
1左移16位等于65536,每次获取读锁成功都会加上65536,通过以下代码将65536右移16位,实际上每次获取读锁还是只是会加1,就好比你早上去上班去公司,晚上下班回家,最终晚上你还是回家了,左移,右移也是这个道理,是一个过程,最终的状态没变。
//偏移位数
static final int SHARED_SHIFT = 16;
//获取读锁重入数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
上面shareCount
函数通过位运算是做无符号右移16
位获取读锁的重入数,为什么可以获取到呢?
1左移16位为65536,65536再右移16位为1。
3的二进制
0000 0000 0000 0000 0000 0000 0000 0011
3右移16位
0000 0000 0000 0011 0000 0000 0000 0000
比如我们获取到了3
次读锁,就是65536 * 3 = 196608
,转换下公式就是3
左移16
位等于196608
,196608
右移16
位等于3
。
虽然我们每次获取到读锁都会+65536
,但是获取读锁时会做右移16
位,所以效果和+1
是一样。
//偏移位数
static final int SHARED_SHIFT = 16;
//获取低16位的条件
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//获取写锁重入数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
剩下的写锁就非常简单,获取低16
位不用左右移动,只要把高16
位全部补0
即可。
反推一下,因为不需要左右移动,其实就和正常的数字一样,只不过因为高16
位补0
,导致数值范围在0~65535
,也就是说写锁获取成功直接+1
就好了。
EXCLUSIVE_MASK
变量,1
右移16
位后-1
,得到65535
,65535
的二进制就是111111111111111
。
现在来看exclusiveCount
函数,该函数内做了位运算&
,&
又称"与"运算。
"与"运算是两个二进制,每位数运算,运算规则如下
0&0=0
0&1=0
1&0=0
1&1=1
如果相对应位都是1,则结果为1,否则为0
可能有些读者大大还是不太明白,下面放张图16
位二进制"与"运算图
我们发现"与"运算时,只要有一方为0
,那结果一定是0
,所以为了切割低16
位,可以使用&
来完成。
从上图可以看出,EXCLUSIVE_MASK
高16
位都是0
,低16
位都是1
,和它&
的变量,高16
位全部会变成0
,低16
位全部保留下来,最终达到获取低16
位效果。
c & EXCLUSIVE_MASK
,假设c
是1
,&
的过程如下图
这样看可能没太大感觉,我们把数值调大点,假设c
是65536
和65537
,&
的过程如下图
现在有感觉了吧,c
的高16
位都会变成0
,低16
位会原样保留,最终达到获取低16
位效果。
EXCLUSIVE_MASK
范围在0~65535
,所以c
的范围也不会超过0~65535
,因为超过了也会通过& EXCLUSIVE_MASK
回到0~65535
。
ReentranReadWriteLock的写锁的获取与释放实现基于同步器AbstractQueuedSynchronizer,与其它独占锁的差异就在于对同步器中tryAcquire()和tryRelease()的重写上,所以下面仅讨论这两个方法。AbstractQueuedSynchronizer独占锁释放获取实现见:AbstractQueuedSynchronizer独占式同步状态获取与释放。
官方给出的注释如下:
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. 若读锁数或写锁数非空,并且当前线程不是读锁或写锁的持有者,获取失败。
* 2. 若当前线程获取写锁的数量已经达到最大值,失败。
* 3. 否则,当前线程获得锁
*/
Thread current = Thread.currentThread(); //返回当前线程对象
int c = getState();
int w = exclusiveCount(c);
if (c != 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;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
释放方法很简单,就是对锁的状态进行判断,无误则对状态进行修改。
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;
}
isHeldExclusively()方法
读锁是否被当前线程持有
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
getExclusiveOwnerThread()方法
获取锁持有者线程
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
读锁是一个支持重进入的共享锁,他能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功的获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
当写锁被另外一个线程持有时,获取读锁失败;否则调用readerShouldBlock()来判断这次获取读锁的操作应不应该阻塞,然后判断读锁数是否达到上限,如果上面两次判断都通过,就调用compareAndSetState()来更新锁状态。
readerShouldBlock()方法主要是为了避免尝试获取写锁的线程陷入无限阻塞,如果队列的头结点是一个等待的writer节点,那么该方法就会返回true。
通过下面代码可以看到,在调用compareAndSetState()更新锁状态成功后,会进入if方法体。这段代码的作用主要是:更新第一个获取读锁的线程指针和获取次数(firstReader 和firstReaderHoldCount),更新上一个获取读锁的线程指针(cachedHoldCounter),最后更新当前线程获取读锁的数量(通过ThreadLocal存在自己线程的容器中)。若读锁数量为0,表示当前线程是第一个,执行下面1处代码;若当前线程是第一个,然后现在再次获取了,执行下面2处代码;否则更新当前线程的获取读锁数和cachedHoldCounter,最后获取读锁数都+1。最后返回1。
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. 如果写锁被另一个线程持有,则获取读锁失败。
* 2. 否则调用readerShouldBlock()来判断这次获取读锁的操作
* 应不应该阻塞,然后判断读锁数是否达到上限。
* 3. 如果上面两次判断都通过,就调用compareAndSetState()来更新锁状态。
*/
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current) // 写锁被其它线程持有
return -1;
int r = sharedCount(c); // 读锁数量
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { // 读锁+1
if (r == 0) {
firstReader = current;
//1、当前线程是第一个获取读锁的线程
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//2、当前线程是第一个,然后现在再次获取了
firstReaderHoldCount++;
} else {
//3、更新当前线程的获取读锁数,然后更新cachedHoldCounter
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);
}
首先更新firstReader,然后更新cachedHoldCounter和当前线程的锁数量,最后更新锁的状态。同时若读写锁数量都为0,则返回true,这是为了上层方法唤醒阻塞的writer线程;否则返回false。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();//获取当前线程
if (firstReader == current) {
// 当前线程是第一个获取读锁的线程
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter; // 上一个获取读锁的线程
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get(); // rh指向当前线程获取读锁的数量对象
int count = rh.count; // 当前线程获取读锁的数量
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count; // 若当前线程读锁数>1,则数量-1
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT; // 读写锁的数量
if (compareAndSetState(c, nextc)) // 更新锁状态
// 若读写锁数量都为0,则返回true,这里是为了唤醒阻塞的writer线程
return nextc == 0;
}
}
与ReentrantLock一样,ReentrantReadWriteLock也分为公平锁和非公平锁,可以在构造方法中指定,不指定默认就是非公平锁。公平锁和非公平锁都继承自内部同步器Sync,它们的区别仅在writerShouldBlock()和readerShouldBlock()这两个方法的实现上。writerShouldBlock()在tryAcquire()方法中即获取写锁的时候会调用,readerShouldBlock()在tryAcquireShared()方法中即获取写锁的时候会调用。
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
就是在构造方法的时候给sync属性传入FairLock或NonFairLock对象,然后接下来就用sync来进行锁的操作了。
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
final Sync sync; // 构造方法中初始化成FairLock或NonFairLock,进行lock和unlock的主要对象
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public ReentrantReadWriteLock() { this(false); }
// 自定义同步器,tryAcquire()、tryRelease()等主要方法都在里面实现了
abstract static class Sync extends AbstractQueuedSynchronizer {...}
// 公平锁,就是一个同步器,特别实现readerShouldBlock()和writerShouldBlock()来实现公平锁
static final class FairSync extends Sync {...}
// 非公平锁,就是一个同步器,特别实现readerShouldBlock()和writerShouldBlock()来实现非公平锁
static final class NonfairSync extends Sync {...}
writerShouldBlock()和readerShouldBlock()都是判断当前节点有没有不是头结点的前驱节点,即是否有线程比当前线程等待更久的时间,有的话返回false,否则true。
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors(); // 判断当前节点有没有不是头结点的前驱节点
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
获取写锁的线程一定不会阻塞,因为始终返回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(); // 队列中第一个节点是否是writer节点
}
}