回顾
前面我们已经看了写锁是怎样进行获取和释放的,下面就来看看读锁的获取和释放
读锁
写锁是一个排他锁,而读锁却是一个共享锁,而且还支持可重入,并且能被多个线程同时获取
读锁的获取
可以看到,其底层的实现也是AQS,只不过是AQS的共享模式,并且执行的是AQS的acquireShared方法
源码如下
public final void acquireShared(int arg) {
//执行tryAcquireShared方法尝试进行获取共享锁
if (tryAcquireShared(arg) < 0)
//获取共享锁失败,进行CAS自旋(写锁被获取了)
doAcquireShared(arg);
}
可以看到获取共享锁的主要方法为tryAcquireShared
tryAcquireShared
源码如下
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
//依然从注释上开始看起,注释同样提到了三点
//1.如果其他线程拥有了写锁,获取共享锁失败
//2.如果没有写锁,就代表可以获取共享锁,不过要现判断是否应该进行阻塞(由于队列策略)
// 并且通过CAS的方法更新同步状态,更新成功就代表获取锁成功
//3.如果获取共享锁失败,代表CAS失败或者共享锁数量满了
//获取当前线程
Thread current = Thread.currentThread();
//获取当前同步状态量
int c = getState();
//获取低位,也就是获取写锁的同步状态量
//判断写锁的同步状态量是否为0,即有没有线程去获取写锁
//如果写锁被人获取了,再判断写锁的拥有者是不是当前线程
//因为规定拥有写锁的线程可以去拥有读锁
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
//如果写锁被获取了,且写锁不是被自己获取
//获取共享锁失败,返回-1
return -1;
//获取共享锁的同步状态量
int r = sharedCount(c);
//判断获取共享锁需要不需要进行阻塞
//判断共享锁的同步状态量是不是达到了最大值
//判断CAS改变共享锁的同步状态量是否成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//如果共享锁的同步状态量为0,证明是第一个去获取共享锁
//注意此时这里会有并发问题
//假如第一个线程在CAS这里停止了,那么后面线程获取的同步状态量依然为0
if (r == 0) {
//将第一个获取共享锁的线程设置为当前线程
//这里是有并发问题的
firstReader = current;
//记录第一个线程获取共享锁的次数
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//如果第一个获取共享锁的线程,又去获取共享锁
//次数+1,代表重入了
firstReaderHoldCount++;
} else {
//如果是其他线程进来获取共享锁
//定义一个HoldCounter缓存
HoldCounter rh = cachedHoldCounter;
//HoldCounter缓存存储在ThreadLocalMap中
if (rh == null || rh.tid != getThreadId(current))
//如果缓存为空,代表第一次其他线程进来
//如果缓存不为空,但此时的缓存不属于当前线程(线程ID不对应)
//从ThreadLocal的ThreadLocalMap中去取当前线程的缓存
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
//如果缓存中的count为0
//将缓存添加进容器中
readHolds.set(rh);
//通过holdCounter记录其他线程获取的共享锁次数
rh.count++;
}
//返回1,代表加锁成功
return 1;
}
//如果CAS失败或其他原因导致失败
//调用fullTryAcquireShared方法
//其实里面底层是
return fullTryAcquireShared(current);
}
从代码上可以看到,共享锁其实还挺复杂
-
判断写锁有没有被线程获取
- 如果写锁被获取了,再判断写锁是不是被当前线程获取,如果是当前线程获取,仍然可以继续去获取读锁
- 如果写锁被获取了,且并不是当前线程获取的,直接reutnr -1,代表获取读锁失败
-
判断是否需要进行阻塞
-
判断读锁的同步状态量是否达到最大值
-
判断CAS更换读锁的同步状态量是否成功
-
只有满足上面三个判断,才代表获取锁成功
-
获取锁成功之后就要进行记录重入次数了,虽然我们更改了读锁的同步状态量,但读锁的同步状态量是一个整体的,而读锁是共享的,即每个线程都有,所以需要每个线程去记录读锁的重入次数
- 对于第一个获取共享锁的线程,进行记录,并且记录其重入次数
- 对于其他线程,是使用ThreadLocal来进行存储的,具体的底层容器为ThreadLocalMap
-
如果获取锁失败,会调用fullTryAcquireShared方法,这个方法的本质上是CAS进行tryAcquiredShared。。。
前面的步骤都还好理解,重点在于读锁怎么去存储各个线程的重入次数的
记录各个线程的重入次数
对于被firstReader记录的线程还好说,通过firstReaderHoldCount进行记录即可
但对于其他线程,则是利用ThreadLocalMap来进行存储的
HoldCounter
每个线程对应的缓存其实就是一个HoldCounter
可以看到,这个类里面有两个属性
- count:记录重入次数
- tid:记录线程ID
从前面可以看到,当前缓存为空,或者该缓存并不属于当前线程(线程ID不对应),那么就会走readHolds的get方法
从判断条件可以看到,这个get方法应该会做两种事情
- 缓存为空,证明没创建,需要进行初始化
- 缓存不属于当前线程,需要进行切换线程
从源码中可以看到,readHolds本质上是一个ThreadLocalHoldCounter对象,并且继承了ThreadLocal,并且泛型是HoldCounter,所以ThreadLocalHoldCounter拥有ThreadLocal的功能,而ThreadLocal有存储功能,可以将元素存储在ThreadLocalMap中
下面就来看看get方法是做什么的
readHolds.get
可以看到,这个方法来自ThreadLocal源码如下
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap不为空
if (map != null) {
//根据当前ThreadLocal去获取存储的键值对
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//如果键值对不为空,返回value
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果ThreadLocalMap为空,需要进行初始化
return setInitialValue();
}
从这里大概就可以看到大致的模型了
当前线程的ThreadLocalMap中存储了以ThreadLocalHoldCounter的父类ThreadLocal为键的,HoldCounter为值的键值对,键是每个线程都能拿到的,但这里能进行区分是因为每个线程都存在了自己的ThreadLocalMap中,这样就进行区分了
可以看到,其底层其实是一个Entry数组
setInitialValue方法
这个方法其实就是当ThreadLocalMap没有创建的时候,需要进行初始化的时候调用
*/
private T setInitialValue() {
//获取默认值
//默认值为null
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//再次判断有没有创建
ThreadLocalMap map = getMap(t);
if (map != null)
//如果已经创建,直接set方法改变里面的entry属性
map.set(this, value);
else
//如果没创建,就进行创建
createMap(t, value);
return value;
}
set方法
当count属性为0时,会调用set方法,这个方法其实是用来修改值的或者为空时进行初始化的,没太看懂这个是干嘛的。。是为0时还没有进入缓存吗?
最后初始化缓存和容器了之后,就进行对缓存的count属性进行自增1了,这样就对应记录了每个线程的重入次数了
假如第一次获取失败
第一次获取失败是会执行fullTryAcquireShared方法的,这个方法名也挺有讲究,翻译为完全尝试,前面先尝试,尝试失败就进行完全尝试
源码如下
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = null;
//死循环
//已经在开始自旋获取锁了
for (;;) {
//获取锁的状态量
int c = getState();
//获取写锁的状态量
if (exclusiveCount(c) != 0) {
//并且判断,如果有人获取了写锁,并且该写锁的拥有者不是自己,返回-1
//交由AQS去实现共享锁了
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
}
//如果没人获取写锁,或者写锁的拥有者是自己
//判断获取读锁需不需要进行阻塞
//这个方法对于公平锁和非公平锁有不同实现
//为什么要判断要阻塞了,这就涉及到公平与非公平的性质
//公平的话,肯定是要进行排队的,非公平的话不需要进行排队
else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
//如果进来需要进行阻塞,也就意味着需要进行排队,即前面已经有线程等待了
//这里是保证当前线程没有以可重入的方式获取锁的
//因为如果是以可重入的方式去获取锁,那就没有什么插不插队的概念了
//就比如买票看电影一样,你已经排队买了电影票了,然后有事离开了一下,那么下一次进来还需要排队买票吗?
//判断firstReader是不是当前线程,如果是就不用清除了
//因为第一个获取读锁的线程由ReentrantReadWriteLock维护
if (firstReader == current) {
//并且如果是第一个线程获取的,那就代表发生可重入了,没有插队概念
//不做任何处理,继续获取锁
// assert firstReaderHoldCount > 0;
} else {
//如果第一个获取读锁的线程不是自己,那就代表有其他线程已经拿到锁了
//下面就需要进行判断,当前线程获取读锁需不需要进行排队
if (rh == null) {
//获取缓存,即前一次获取锁的线程
rh = cachedHoldCounter;
//如果为空或者缓存的线程ID不为自己
//代表了
if (rh == null || rh.tid != getThreadId(current)) {
//获取自己ThreadLocal存储的共享锁状态量
rh = readHolds.get();
//如果为0.清空掉
if (rh.count == 0)
//清空掉当前线程的ThreadLocal的读锁状态量
readHolds.remove();
}
}
//为什么此时rh.count仍然为0就return????
//因为进入到这里代表了线程需要进行阻塞,而阻塞是交由AQS决定的
//如果是第一次获取锁,就代表没有发生可重入,那就需要进行排队了
//返回-1,让AQS去实现阻塞排队
if (rh.count == 0)
return -1;
}
}
//判断现在读锁的状态量是不是已经为最大值了
if (sharedCount(c) == MAX_COUNT)
//如果为最大值,代表读锁的数量已经满了,不能获取了,抛出异常
throw new Error("Maximum lock count exceeded");
//如果没满最大值,那就进行CAS替换状态量
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
return 1;
}
//获取锁失败,就进行下一轮循环
}
}
可以看到,这个方法其实是在CAS获取读锁的基础上,添加了阻塞功能,也就是说完整的尝试获取锁,不仅是判断写锁的状态和读锁的状态,还需要去考虑是否要阻塞
而考虑这个阻塞问题,其实就是关于公平锁和非公平锁的判断了,这里要注意一点的就是可重入锁是没有排队这个概念的!!!!