04特性源码分析-ReentrantReadWriteLock原理-AQS-并发编程(Java)

1 锁重入

锁重入就是同一线程在持有锁的情况下,再次获取同一把锁的相同锁。在之前我们分析读写锁源码的时候,不管是加锁还是解锁,底层都提供了对锁重入的支持。这里不再赘述,关于同一线程获取同一把锁的不同锁的情况,在下面锁降级和锁升级分析。

2 锁重入计数

其中写锁是独占锁,锁计数设置在锁状态state中,和之前ReentrantLock一样,这里不再赘述。我们这里详细分析下读锁即共享锁的锁重入计数

2.1 读锁加锁计数

在读锁加锁tryAcquireShared()方法中,我们截取锁重入部分源代码如下:

if (!readerShouldBlock() &&
	r < MAX_COUNT &&
	compareAndSetState(c, c + SHARED_UNIT)) {
	if (r == 0) {
		firstReader = current;
		firstReaderHoldCount = 1;
	} 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;
}
  • 第一个加读锁的线程单独记录

    • firstReader:记录该线程
    • firstReaderHoldCount:该线程重入次数
  • 同一把锁在加读锁期间,有其他读锁线程,那么就通过设置的ThreadLocal来计数

    • readHolds:ThreadLocalHoldCounter类型继承自ThreadLocal

    • cachedHoldCounter:HoldCounter一个简单的计数器

      • count:记录锁重入次数
      • tid:记录当前线程id
    • 相关源代码如下:

      static final class ThreadLocalHoldCounter
      	extends ThreadLocal<HoldCounter> {
      	public HoldCounter initialValue() {
      		return new HoldCounter();
      	}
      }
      
      static final class HoldCounter {
      	int count = 0;
      	// Use id, not reference, to avoid garbage retention
      	final long tid = getThreadId(Thread.currentThread());
      }
      
  • 其他读锁线程第一次尝试获取锁成功之后,通过readHolds把新创建的HoldCounter绑定到当前线程,且计数+1。

    • HoldCounter初始计数为0
  • 其他读锁线程非一次尝试获取锁成功之后,通过readHolds获取当前线程本地变量HoldCounter,且计数+1。

注:ThreadLocal相关原理分析,可以查看02源码分析-ThreadLocal详解-并发编程(Java)系列文章,或者自行查阅次相关文档。

2.2 读锁解锁计数

截取解锁方法tryReleaseShare()中关于锁计数部分源代码如下:

Thread current = Thread.currentThread();
if (firstReader == current) {
	// assert firstReaderHoldCount > 0;
	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;
}

也是第一个加读锁线程和其他加读锁的线程分开执行。

  • 判断是第一个加读锁的线程,相应计数-1,如果此时计数是1firstReader置空。
  • 其他读锁线程先获取缓存计算器,如果为空,通过readHolds获取当前线程的计数器;如果计数为1移除相应的计数器。最后计数-1。

3 公平与非公平锁

3.1 非公平锁

读写锁非公平实现源代码如下:

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();
    }
}
  • writerShouldBlock():直接返回false,即读写锁在非公平实现下,写锁在尝试获取锁时不应该被阻塞。
  • readerShouldBlock():调用了apparentlyFirstQueuedIsExclusive()方法

apparentlyFirstQueuedIsExclusive()方法源代码如下:

final boolean apparentlyFirstQueuedIsExclusive() {
	Node h, s;
	return (h = head) != null &&
		(s = h.next)  != null &&
		!s.isShared()         &&
		s.thread != null;
}

该方法只有在锁竞争队列不为空,且第二个线程结点非共享结点且线程不为空的情况下才返回true;否则返回false。即读写锁在非公平实现下,读锁在尝试获取锁时,在锁竞争队列不为空,且第二个线程结点非共享结点且线程不为空的情况下读锁应该被阻塞;否则读锁不会被阻塞。

3.2 公平锁

读写锁公平实现源代码如下:

static final class FairSync extends Sync {
	private static final long serialVersionUID = -2274990926593161451L;
	final boolean writerShouldBlock() {
		return hasQueuedPredecessors();
	}
	final boolean readerShouldBlock() {
		return hasQueuedPredecessors();
	}
}
  • writerShouldBlock():调用hasQueuedPredecessors()方法,判断锁竞争队列是否有前驱将结点,源代码如下

    public final boolean hasQueuedPredecessors() {
    	// The correctness of this depends on head being initialized
    	// before tail and on head.next being accurate if the current
    	// thread is first in queue.
    	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());
    }
    
    • 队列为空,没有;否则有前驱结点
    • 即读写锁在公平实现下,写锁在尝试获取锁时如果有前驱结点应该被阻塞;否则不应该被阻塞。
  • readerShouldBlock():调用的同一方法hasQueuedPredecessors().

    • 即读读锁在公平实现下,读锁在尝试获取锁时如果有前驱结点应该被阻塞;否则不应该被阻塞。

4 锁降级与锁升级

同一线程在获取同一把锁的写锁情况下,在次获取该锁的读锁,称为锁降级。同一线程在获取同一把锁的读锁情况下,在次获取该锁的写锁,称为锁升级。

4.1 锁升级

锁升级的情况,底层是不支持的,下面截取写锁加锁方法tryAcquire()关于读锁再次获取写锁情况的代码如下:

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;
}
  • 当前已经加读锁,c!=0
    • 因为加了读锁,写锁计数w肯定是0啊,此时直接返回false,即尝试加写锁失败

4.2 锁降级

锁降级的情况,下面截取读锁加锁方法tryAcquireShared()关于写锁再次获取读锁情况的代码如下:

if (exclusiveCount(c) != 0 &&
	getExclusiveOwnerThread() != current)
	return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
	r < MAX_COUNT &&
	compareAndSetState(c, c + SHARED_UNIT)) {
	// 此时省略
	}
return fullTryAcquireShared(current);
  • 因为当前是获取写锁的情况下,同一线程再次获取读锁,第一步的判断getExclusiveOwnerThread() != current,false,程序继续向下执行
  • 第二步的判断,三个条件
    • 第二个判断条件r < MAX_COUNT:MAX_COUNT的值为 2 16 − 1 2^{16}-1 2161,一般不会出现读锁计数大于最大容量的情况
      • 同一线程读锁重入 2 16 − 1 2^{16}-1 2161次或者不同线程同一读写锁的读锁计数超过 2 16 − 1 2^{16}-1 2161次,你能想象这是什么情况吗?
    • 第三个判断条件compareAndSetState(c, c + SHARED_UNIT):因为当前线程持有写锁,如果程序执行到这儿cas会执行成功
    • 那么影响第二步判断整体为true还是false在于第一个判断条件!readerShouldBlock(),这个方法我们在上面公平和非公平中有讲解
      • !readerShouldBlock():注意是**!**readerShouldBlock
        • 非公平:只有锁竞争队列不为空且第二个结点为非共享结点(这里写锁线程结点)且线程不为空的情况下为false;其他为ture
        • 公平:锁竞争队列有前驱结点为false;其他为true。
  • 第二步的判断为true,即锁降级成功。
    • 锁降级成功情况下,记得用完释放读锁写锁
  • 第二步判断如果为false,会执行fullTryAcquireShared()方法,看下源代码如下
final int fullTryAcquireShared(Thread current) {
	HoldCounter rh = null;
	for (;;) {
		int c = getState();
		if (exclusiveCount(c) != 0) {
			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
			if (firstReader == current) {
				// assert firstReaderHoldCount > 0;
			} 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");
		if (compareAndSetState(c, c + SHARED_UNIT)) {
			// 省略
		}
	}
}
  • 此时,for循环的第一个判断exclusiveCount© != 0 为ture
    • getExclusiveOwnerThread() != current 为false
    • 此处造成死锁

锁降级如果写锁在尝试获取读锁而读锁在不应该被阻塞的情况下,此时没有释放写锁就会发生死锁。

5 fullTryAcquireShared()造成死锁

源代码贴在上面,下面分析会造成死锁原因,这里的死锁准确是说线程饥饿,一直在死循环。

  • 第一种情况在上面锁降级时,已经分析
  • 在if (exclusiveCount© != 0)条件不成立进入判断readerShouldBlock()为true的情况
    • exclusiveCount© == 0即当前加的是读锁的情况
    • readerShouldBlock()为true 写锁应该被阻塞的情况,公平与非公平中已经讲解。
    • 此时如果是第一个加读锁的线程,在没有释放的情况下也会死锁
    • 如果不是第一个读锁的线程,在没有释放的情况下也会死锁

读写锁的锁重入慎用。

6 条件变量

读写锁的读锁不支持条件变量;写锁支持条件变量。

  • 读锁共享,不需要支持条件变量,如果支持条件变量的话有什么实际意义呢?
  • 写锁条件变量同ReentrantLock,不在赘述。

7 后记

如有问题,欢迎交流讨论。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gaog2zh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值