摘自网上一段话:
ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。
前文Java锁Lock源码分析(一)提过在java的Lock中获取锁就表示AQS的volatile int state =1表示获取到了独占锁,state>1表示当前线程重入锁(获取锁了再次获取到了锁)即大于0就表示获取到了独占锁。
独占就意味着排队,失败,系统吞吐量下降,用户体验下降等等。有些情况不要独占,比如说读与读不互斥,读与写互斥(但是写锁可降级为读锁),写与写互斥的这样会提升系统的吞吐量。读写锁就是完成这个功能的主角,先上一段代码坐下证明:
这个结果已经说明问题了,读取的时候没有写入,写入的时候没有其他写入,没有其它读,读的时候有其它读,很直白。
那么问题就来了,之前说过,AQS的成员volatile int state就是表示获取成功了锁,那么读写锁是怎么做到读与读不互斥而共享的,写与写是怎么互斥的。
1)AQS的volatile int state;int类型是32位的高16位表示写锁,低16位表示读锁。一个字段技能表示读锁,也能表示写锁。
2)实现原理:写锁即低16位c & (1<<16 -1) 读锁即高16位 c >>> 16得出的值表示AQS的state
ReentrantReadWriteLock的抽象AQS实现Sync
源码中有几行注释方法和属性就是上面两句话的总结:
上面的代码已经很清楚的演示了ReentrantReadWriteLock的使用方法。
读锁:
我们先讲写锁的获取(相比于读锁它看起来更加简单)
ReentrantReadWriteLock.WriteLock.lock()
public void lock() {
sync.acquire(1);
}
//同样的还是进入到了AQS的模板方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
主要还是看下ReentrantReadWriteLock是如何重写tryAcquire(1)方法的:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
//1)w=c&(1<< 16-1)写锁的数量 大于0一定是同一个线程重入的
int w = exclusiveCount(c);
//2)有线程占用过锁
if (c != 0) {
//3)被写锁独占,不是当前线程(其他线程获取到的写锁)则失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//4)被写锁独占,而且是当前线程重入的次数大于1<<16-1则抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//5)被写锁独占,是当前线程获取到的锁,重入
setState(c + acquires);
return true;
}
//6)没有线程持有锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
上面的注释已经是非常清楚,这里只是对第6)做下补充:
既然一下线程也没有获取到锁为什么不直接cas设置抢占锁呢?不要忘记ReentrantReadWriteLock也是支持公平锁和费公平锁的,也支持重入锁
公平锁)FailSync判断AQS的双向链表有没有节点,有的话直接返回失败,没有的话CAS抢占一下 –公平的体现 没有排队才抢占一下,所以writeShouldBlock是判断队列中有无排队的。
非公平锁)NonfairSync直接不管有没有排队的直接抢一下 所以writeShouldBlock什么也不判断直接返回false
PS:6)中失败即公平锁队列中有排队的或者 cas在失败,改线程是如何进入到队列(双向链表)的可以参考之前的两篇文章Java锁lock源码分析(一),这里不做解释了。
ReentrantReadWriteLock.ReadLock.lock()
public void lock() {
sync.acquireShared(1);
}
还是走的AQS的模板方法:
PS:JUC包下的几个重要线程同步工具类如Semaphore、CountDownLatch、ReentrantReadWriteLock正式共享锁的实现,它们都是重写了AQS的tryAcquireShared(args)方法。
我们看下在ReentrantReadWriteLock中的tryAcquireShared方法的具体实现:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//1)判断写锁 c & (1<<16-1)!=0,写锁不是当前线程
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//2)c >>>16
int r = sharedCount(c);
//3)等待队列没有写线程,成功获取到读锁的线程数少于1<<16-1,cas设置成功
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;
}
//cas不成功,超出读锁数量,或则写线程在排队 多次重试
return fullTryAcquireShared(current);
}
补充下:
对于1)判断有无写锁跟读锁互斥这个这正常,后边还有一个判断独占写锁是否是当前线程—写锁是可以降级为读锁的
对于3)那读锁来说AQS的state通过计算sharedCount(state)表示的读锁的总数,但是每个线程能获取多少个锁是没有统计,后期的释放锁就会有问题所以多出来了HoldCounter来表示当前线程获取锁的个数(能重入的)
读写线程是如何进入到等待队列,公平锁与非公平锁的区别,如何重入以前都说过Java锁lock源码分析(一),我们只需要知道读写锁还是操作的AQS的state变量,高位表示写锁,低位表示读锁,
ReentrantReadWriteLock的抽象AQS实现Sync
获取锁的总数量:
static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;
private transient HoldCounter cachedHoldCounter;
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}