前言
线程并发系列文章:
Java 线程基础
Java 线程状态
Java “优雅”地中断线程-实践篇
Java “优雅”地中断线程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
线程池必懂系列
上篇文章分析了AQS的实际应用之一:ReentrantLock 的实现。ReentrantLock 和synchronized 都是独占锁,而AQS还支持共享锁,本篇就来分析AQS 共享锁的实际应用。
通过本篇文章,你将了解到:
1、共享锁、独享锁区别
2、读锁的实现原理
3、写锁的实现原理
4、读写锁 tryLock 原理
5、读写锁的应用
1、共享锁、独享锁区别
基本差别
共享锁、独占锁是在AQS里实现的,核心是"state"的值:
image.png
如上图,对于共享锁来说,允许多个线程对state进行有效修改。
读写锁的引入
根据上面的图,state 同时只能表示一种锁,要么独占锁,要么共享锁。而在实际的应用场景里经常会碰到多个线程读,多个线程写的情况,此时为了能够协同读、写线程,需要将state改造。
先来看AQS state 定义:
#AbstractQueuedSynchronizer.java
private volatile int state;
可以看出是int 类型的(当然也有long 类型的,在AbstractQueuedLongSynchronizer.java 里,本文以int 为例)
state 被分为两部分,低16位表示写锁(独占锁),高16位表示读锁(共享锁),这样一个32位的state 就可以同时表示共享锁和独占锁了。
2、读锁的实现原理
ReentrantReadWriteLock 的构造
ReentrantReadWriteLock 并没有像ReentrantLock一样直接实现Lock 接口,而是内部分别持有ReadLock、WriteLock类型的成员变量,两者均实现了Lock 接口。
#ReentrantReadWriteLock.java
public ReentrantReadWriteLock() {
//默认非公平锁
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
//构造读锁
readerLock = new ReadLock(this);
//构造写锁
writerLock = new WriteLock(this);
}
ReentrantReadWriteLock 默认实现非公平锁,读锁、写锁支持非公平锁和公平锁。
读写锁构造之后,将锁暴露出来给外部使用:
#ReentrantReadWriteLock.java
//获取写锁对象
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
//获取读锁对象
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
获取锁
在ReentrantLock 分析独占锁时有如下图:
image.png
与独占锁类似,AQS虽然已经实现了共享锁的基本逻辑,但是真正获取锁、释放锁的操作还是需要子类实现,共享锁需要实现方法:
tryAcquireShared & tryReleaseShared
来看看获取锁的过程:
#ReentrantReadWriteLock.ReadLock
public void lock() {
//共享锁
sync.acquireShared(1);
}
#AbstractQueuedSynchronizer.java
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
//doAcquireShared 在AQS里实现
doAcquireShared(arg);
}
重点是tryAcquireShared(xx):
#ReentrantReadWriteLock.java
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//此处exclusiveCount作用是取state 低16位,若是不等于0,说明有线程占有了写锁
//若是有线程占有了写锁,而这个线程不是当前线程,则直接退出------------>(1)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//获取state 高16位,若是大于0,说明有线程占有了读锁
int r = sharedCount(c);
//当前线程是否应该阻塞
if (!readerShouldBlock() &&//------------>(2)
r < MAX_COUNT &&//若是不该阻塞,则尝试CAS修改state高16位的值
compareAndSetState(c, c + SHARED_UNIT)) {
//--------记录线程/重入次数----------->(3)
//修改state 成功,说明成功占有了读锁
if (r == 0) {
//记录第一个占有读锁的线程
firstReader = current;
//占有次数为1
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//第一个占有读锁的线程重入了该锁
firstReaderHoldCount++;
} else {
//是其它线程占有锁
//取出缓存的HoldCounter
HoldCounter rh = cachedHoldCounter;
//若是缓存为空,或是缓存存储的不是当前的线程
if (rh == null || rh.tid != getThreadId(current))
//从threadLocal里获取
//readHolds 为ThreadLocalHoldCounter 类型,继承自ThreadLocal
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
//说明cachedHoldCounter 已经被移出threadLocal,
//重新加入即可------------>(4)
readHolds.set(rh);
//记录重入次数
rh.count++;
//--------记录线程/重入次数-----------
}
return 1;
}
//------------>(5)
return fullTryAcquireShared(current);
}
以上是获取读锁的核心代码,标注了5个重点,分别来分析。
(1)
此处表明了一个信息:
若是当前线程已经获取了写锁,那么它可以继续尝试获得读锁。
当它把写锁释放后,只剩读锁了。这个过程可以理解为锁的降级。
(2)
线程能否有机会获取读锁,还需要经过两个判断:
1、判定readerShouldBlock()。
2、判定读锁个数用完了没,阈值是2^16-1。
而读锁公平与否就体现在readerShouldBlock()的实现上。
先来看非公平读锁:
#ReentrantReadWriteLock.java
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
#AbstractQueuedSynchronizer.java
final boolean apparentlyFirstQueuedIsExclusive() {
//判断等待队列里的第二个节点是否在等待写锁
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
若等待队列里的第二个节点是在等待写锁,那么此时不能去获取读锁。
这与ReentrantLock不一样,ReentrantLock 非公平锁的实现是不管等待队列里有没有节点,都会去尝试获取锁。
再来看公平读锁
#ReentrantReadWriteLock.java
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
判断队列里是否有更早于当前线程排队的节点,该方法在上篇分析ReentrantLock 时有深入分析,此处不再赘述。
(3)
这部分代码看起来多,实际上就是为了记录重入次数以及为了效率考虑引入了一些缓存。
考虑到有可能始终只有一个线程获取读锁,因此定义了两个变量还记录重入次数:
#ReentrantReadWriteLock.java
//记录第一个获取读锁的线程
private transient Thread firstReader = null;
//第一个获取读锁的线程获取读锁的个数
private transient int firstReaderHoldCount;
再考虑到有多个线程获取锁,它们也需要记录获取锁的个数,与线程绑定的数据我们想到了ThreadLocal,于是定义了:
private transient ThreadLocalHoldCounter readHolds;
来记录HoldCounter(存储获取锁的个数及绑定的线程id)。
最后为了不用每次都去ThreadLocal里查询数据,再定义了变量来缓存HoldCounter:
#ReentrantReadWriteLock.java
private transient HoldCounter cachedHoldCounter;
(4)
cachedHoldCounter.count == 0,是在tryReleaseShared(xx)里操作的,并且判断当线程已经彻底释放了读锁后,将HoldCounter 从ThreadLocal里移除,因此此处需要加回来。
(5)
走到这一步,说明之前获取锁的操作失败了,原因有三点:
1、readerShouldBlock() == true。
2、r >= MAX_COUNT。
3、中途有其它线程修改了state。
fullTryAcquireShared(xx)与tryAcquireShared(xx)很类似,目的就是为了获取锁。
针对第三点,fullTryAcquireShared(xx)里有个死循环,不断获取state值,若是符合1、2点,则退出循环,否则尝试CAS修改state,若是失败,则继续循环获取state值。
小结一下:
1、fullTryAcquireShared(xx) 获取锁失败返回-1,接下来的处理逻辑流转到AQS里,线程可能会被挂起。
2、fullTryAcquireShared(xx) 获取锁成功则返回1。
释放锁
释放锁的逻辑比较简单:
#ReentrantReadWriteLock.ReadLock
public void lock() {
sync.acquireShared(1);
}
#AbstractQueuedSynchronizer.java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//在AQS里实现
doReleaseShared();
return true;
}
return false;
}
重点是tryReleaseShared(xx):
#ReentrantReadWriteLock.java
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))
//取不到,则需要从ThreadLocal里取
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
//若是当前线程不再占有锁,则清除对应的ThreadLocal变量
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
//修改state
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
//若是state值变为0,说明读锁、写锁都释放完了
return nextc == 0;
}
}
此处需要注意的是:
tryReleaseShared(xx)释放读锁时候,若是没有完全释放读锁、写锁,那么将会返回false。
而在AQS里释放共享锁流程如下:
#AbstractQueuedSynchronizer.java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
也就是说此种情况下,doReleaseShared() 将不会被调用,也就不会唤醒同步队列里的节点。
这么做的原因是:
若只释放完读锁,还剩写锁被占用。而因为写锁是独占锁,其它线程无法获取锁,那么即使唤醒了它们也没有用。
3、写锁的实现原理
获取锁
写锁是独占锁,因此重点关注tryAcquire(xx):
#ReentrantReadWriteLock.java
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//获取当前写锁个数
int w = exclusiveCount(c);
if (c != 0) {
//1、若是w==0,而c!= 0,说明有线程占有了读锁,不能再获取写锁了
//2、若是写锁被占用,但是不是当前线程,则不能再获取写锁了
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//锁个数超限了
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//走到此处,说明重入,直接设置,同一时刻只有一个线程能走到这
setState(c + acquires);
return true;
}
//若c==0,此时读锁、写锁都没线程占用
//判断线程是否应该被阻塞,否则尝试获取写锁------->(1)
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//独占锁需要关联线程
setExclusiveOwnerThread(current);
return true;
}
来看看writerShouldBlock(),写锁公平/非公平就在此处实现的。
先来看非公平写锁:
#ReentrantReadWriteLock.java
final boolean writerShouldBlock() {
//不阻塞
return false; // writers can always barge
}
非公平写锁不应该阻塞。
再来看公平写锁:
#ReentrantReadWriteLock.java
final boolean writerShouldBlock() {
//判断队列是否有有效节点等待
return hasQueuedPredecessors();
}
和公平读锁一样的判断条件。
小结
1、读锁/写锁 已被其它线程占用,那么新来的线程将无法获取写锁。
2、写锁可重入。
释放锁
释放锁重点关注tryRelease(xx):
##ReentrantReadWriteLock.java
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;
}
若tryRelease(xx)返回true,则AQS里会唤醒等待队列的线程。
4、读写锁 tryLock 原理
读锁tryLock
#ReentrantReadWriteLock.java
public boolean tryLock() {
return sync.tryReadLock();
}
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
//for 循环为了检测最新的state
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (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 true;
}
}
}
可以看出tryReadLock(xx)里: 只要不是别的线程占有写锁并且读锁个数没超出限制,那么它将一直尝试获取读锁,直到得到为止。
写锁tryLock
public boolean tryLock() {
return sync.tryWriteLock();
}
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current);
return true;
}
写锁只尝试一次CAS,失败就返回。
最终,用图表示读锁、写锁实现的功能:
image.png
读锁与写锁关系:
image.png
5、读写锁的应用
分析完原理,来看看简单应用。
public class TestThread {
static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
public static void main(String args[]) {
//读
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
try {
System.out.println("thread " + threadName + " acquire read lock");
readLock.lock();
System.out.println("thread " + threadName + " read locking");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println("thread " + threadName + " release read lock remain read count:" + readWriteLock.getReadLockCount());
}
}
}, "" + i).start();
}
//写
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
try {
System.out.println("thread " + threadName + " acquire write lock");
writeLock.lock();
System.out.println("thread " + threadName + " write locking");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println("thread " + threadName + " release write lock remain write count:" + readWriteLock.getWriteHoldCount());
}
}
}, "" + i).start();
}
}
}
10个线程获取读锁,10个线程获取写锁。
读写锁应用场景:
- ReentrantReadWriteLock 适用于读多写少的场景,提高多线程读的效率、吞吐量。
同一线程读锁、写锁关系:
public class TestThread {
static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
public static void main(String args[]) {
// new TestThread().testReadWriteLock();------>1、先读锁,后写锁
// new TestThread().testWriteReadLock();------>2、先写锁、后读锁
}
private void testReadWriteLock() {
System.out.println("before read lock");
readLock.lock();
System.out.println("before write lock");
writeLock.lock();
System.out.println("after write lock");
}
private void testWriteReadLock() {
System.out.println("before write lock");
writeLock.lock();
System.out.println("before read lock");
readLock.lock();
System.out.println("after read lock");
}
}
分别打开1、2 注释,发现:
1、先获取读锁,再获取写锁,则线程在写锁处挂起。
2、先获取写锁,再获取读锁,则都能正常获取锁。
这与我们上述的理论分析一致。
下篇将会分析Semaphore、CountDownLatch、 CyclicBarrier原理及其应用。
本文基于jdk1.8。
网友评论