章节概览:
1、概述
通过 Java多线程之AQS(AbstractQueuedSynchronizer )实现原理和源码分析(三)源码分析,我们清楚的知道可重入锁ReentrantLock本质上还是个独占锁,和synchronize实现相同的功能。区别是ReentrantLock是基于乐观锁实现的,而Synchronize是悲观锁实现的。独占锁是一种相对比较保守的策略,在独占锁模式下,只要对共享变量的读,写等操作都不能同时发生。例如 “读/读”,“读/写”,“写/写”。如果这些场景可以同时发生,在独占锁的模式下,可能会出现脏读的现象。那么在高并发的情况下,很明显降低了系统的吞吐量。但是在读数据的过程中,并不会对共享变量的进行修改,只是为了读取数据,并不存在锁竞争的情况。所以,如果存在读写锁这种机制。在读的情况下线程共享的,允许多个线程读取当前数据。在写的情况下是独占的,只允许一个线程进行读写。这样就大大增加了系统的吞吐量。在JUC中,通过ReentrantReadWriteLock类进行实现的。其中分为了读锁和写锁的两种不同的锁。读锁是共享的,写锁是独占的。这样大大增加了读多写少场景的吞吐量。
2、ReentrantReadWriteLock 案例
通过维护一个共享变量Object data,写锁修改当前data的值,读锁读取当前data的值。
public class ReadWriteLockTest {
public static void main(String[] args) {
final Queue q = new Queue();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
q.get();
}
}
}, "RreadLock-" + i).start();
}
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
q.put(new Random().nextInt(10000));
}
}
}, "WriteLock-" + i).start();
}
}
}
class Queue {
/*
*共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
*/
private Object data = null;
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void get() {
//上读锁,其他线程只能读不能写
rwl.readLock().lock();
System.out.println(Thread.currentThread().getName() + " be ready to read data!");
try {
Thread.sleep((long) (Math.random() * 100));
System.out.println("当前线程持有的读线程数: " + rwl.getReadHoldCount());
System.out.println(Thread.currentThread().getName() + "have read data :" + data);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁,最好放在finnaly里面
rwl.readLock().unlock();
}
}
public void put(Object data) {
//上写锁,不允许其他线程读也不允许写
rwl.writeLock().lock();
System.out.println(Thread.currentThread().getName() + " be ready to write data!");
try {
Thread.sleep((long) (Math.random() * 1000));
this.data = data;
System.out.println(Thread.currentThread().getName() + " have write data: " + data);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
rwl.writeLock().unlock();
}
}
}
运行结果如下:
从运行结果中可以发现,读多可以同时出现多次,而写锁每次只能出现一个。
RreadLock-0 be ready to read data!
RreadLock-1 be ready to read data!
RreadLock-2 be ready to read data!
当前线程持有的读线程数: 1
RreadLock-1 have read data :null
当前线程持有的读线程数: 1
RreadLock-2 have read data :null
当前线程持有的读线程数: 1
RreadLock-0 have read data :null
WriteLock-0 be ready to write data!
WriteLock-0 have write data: 924
WriteLock-1 be ready to write data!
WriteLock-1 have write data: 8042
WriteLock-2 be ready to write data!
WriteLock-2 have write data: 1368
RreadLock-1 be ready to read data!
RreadLock-2 be ready to read data!
RreadLock-0 be ready to read data!
3、读写锁的一些规则
-
公平性选择:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
-
重进入:该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁;
-
锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁;
-
锁获取中断:读取锁和写入锁都支持获取锁期间被中断. 这个和独占锁一致;
-
支持条件变量:写入锁提供了条件变量(Condition)的支持, 这个和独占锁一致, 但是读取锁却不允许获取条件变量, 将得到一个UnsupportedOperationException异常。
-
线程进入读锁的前提条件:
- 没有其他线程的写锁。
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
-
线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
官方案例:
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
// 在释放写锁之前通过读取读锁来降级
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
4、ReentrantReadWriteLock 继承关系
从类的继承依赖关系可以看出,ReentrantReadWriteLock 实现了 ReadWriteLock接口。同时其有5个内部类,分别为ReadLock,WriteLock,NonfairSync,Sync,FairSync。其中Sync继承了AbstractQueuedSynchronizer 实现了队列同步器功能,其子类为NonfairSync,FairSync实现了公平和非公平模式。Sync同时也具有两个内部类,分别为:ThreadLocalHoldCounter,HoldCounter用于记录线程拥有读锁的一些信息。当前Sync类和ReentrantLock的Sync类还是有很大的区别的。
5、ReadWriteLock 接口源码分析
ReadWriteLock 定义了获取读写锁的接口。
public interface ReadWriteLock {
// 获取读锁
Lock readLock();
// 获取写锁
Lock writeLock();
}
6、ReentrantReadWriteLock 构造函数和成员变量
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
// 读锁
private final ReentrantReadWriteLock.ReadLock readerLock;
// 写锁
private final ReentrantReadWriteLock.WriteLock writerLock;
// 同步队列
final Sync sync;
// 构造函数,默认是非公平锁
public ReentrantReadWriteLock() {
this(false);
}
// false 为非公平锁,true为公平锁
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
// 初始化成员变量,传入当前实例对象的引用 ReentrantReadWriteLock 实例对象
// 传入当前引用 this 的主要原因是,获取sync的策略
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
// 获取写锁
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
// 获取读锁
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
}
7、Sync 构造方法和核心成员分析
7.1、核心成员变量分析
// 读锁同步状态占用的位
static final int SHARED_SHIFT = 16;
// 每次增加读锁同步状态,就相当于增加SHARED_UNIT
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 读锁或写锁的最大请求数量(包含重入)
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 低16位的MASK,用来计算写锁的同步状态
// 0000 0000 0000 0000 1111 1111 1111 1111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 返回共享锁数
// 右移16位,取高位,低位舍弃
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 返回独占锁数
// c & EXCLUSIVE_MASK 舍弃高位,取低位的值
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。
假设当前同步状态值为S,get和set的操作如下:
(1)获取写状态:
将高16位全部抹去: S & 0000 0000 0000 0000 1111 1111 1111 1111
(2)获取读状态:
S >>> 16: 无符号补0,右移16位
(3)写状态加1:
S + 1
(4)读状态加1:
S +(1<<16)即 S + 0000 0000 0000 0001 0000 0000 0000 0000
7.2、内部类HoldCounter 分析
HoldCounter 静态内部类,主要存储当前线程锁持有的读锁的句柄数
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
7.3、内部类ThreadLocalHoldCounter 分析
ThreadLocalHoldCounter 继承了ThreadLocal,主要用于存储每个线程所持有的读锁的数量。
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
7.4、Sync 构造函数
Sync() {
// 初始化readHolds
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
8、NonfairSync 源码分析
8.1、NonfairSync 类源码
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
// 是否阻塞写锁,在非公平模式下,写锁可以任意竞争
final boolean writerShouldBlock() {
return false; // writers can always barge
}
// 如果AQS的锁等待队列head节点后的节点非共享节点(等待读锁的节点),将返回true。
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
// 请参考8.2 final boolean apparentlyFirstQueuedIsExclusive() 方法
return apparentlyFirstQueuedIsExclusive();
}
}
8.2、 final boolean apparentlyFirstQueuedIsExclusive() 方法
该方法如果头节点不为空,并头节点的下一个节点不为空,并且不是共享模式【独占模式,写锁】、并且线程不为空。则返回true,说明有当前申请读锁的线程占有写锁,并有其他写锁在申请。为什么要判断head节点的下一个节点不为空,或是thread不为空呢?因为第一个节点head节点是当前持有写锁的线程,也就是当前申请读锁的线程,这里,也就是锁降级的关键所在,如果占有的写锁不是当前线程,那线程申请读锁会直接失败。
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
// 非共享节点
!s.isShared() &&
s.thread != null;
}
9、写锁获取释放流程分析
通过上面的分析,我们了解了读写锁的整个类继承关系,核心成员,构造参数等。下面我们根据读写锁的执行流程,进行源码分析。
9.1、java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock#lock
在非公平模式下,调用 java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire 的方法。
public void lock() {
// 参考:9.2、java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
sync.acquire(1);
}
9.2、java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
acquire方法为模板方法,将tryAcquire交给子类进行实现。由于写锁是独占锁,所以addWaiter(Node.EXCLUSIVE) 的方法参数为Node.EXCLUSIVE,这块详细的源码分析参考AbstractQueuedSynchronizer源码分析。
public final void acquire(int arg) {
// 参考:9.3、java.util.concurrent.locks.ReentrantReadWriteLock.Sync#tryAcquire
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
9.3、java.util.concurrent.locks.ReentrantReadWriteLock.Sync#tryAcquire 获取读锁
写锁本身就是个独占锁,其获取的思路和可重入锁的实现思路基本相同
protected final boolean tryAcquire(int acquires) {
// 获取当前调用线程
Thread current = Thread.currentThread();
// 获取当前的队列同步器的state的状态值
int c = getState();
// 获取当前写锁的数量
int w = exclusiveCount(c);
// 如果当前的 c != 0 可能存在两种情况
// 第一种:读锁持有锁
// 第二种:写锁持有锁
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// 当前写锁持有数为 0 ,或者当前线程不是持有写锁的线程,返回false,获取写锁失败
// 第一个条件分析:w == 0 。由于前提条件是 c != 0,而写锁为0,说明当前锁被读锁持有
// 第二个条件分析:current != getExclusiveOwnerThread(),说明写锁支持可重入。由于多线程,可能存在其他线程
// 已经获得写锁,且是同一个线程。
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 判断当前锁的持有量是否大于最大持有锁 MAX_COUNT
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 重入锁,状态值 + 1
setState(c + acquires);
return true;
}
// 如果当前同步器队列的状态 c == 0 的情况下,说明当前没有线程持有读锁或写锁
// 1. 判断当前写锁是否需要被阻塞
// 2.通过CAS算法设置当前State状态
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 设置当前线程持有写锁
setExclusiveOwnerThread(current);
return true;
}
9.3 java.util.concurrent.locks.ReentrantReadWriteLock.Sync#tryRelease 释放读锁
释放锁和可重入锁的逻辑也基本相同,源码分析如下
protected final boolean tryRelease(int releases) {
// 如果当前线程没有持有锁,直接抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 由于写锁是可重入锁,所以释放过后可能还存在持有情况
int nextc = getState() - releases;
// 判断当前写锁个数是否为 0
boolean free = exclusiveCount(nextc) == 0;
if (free)
// 置空持有锁的线程拥有者
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
至此,写锁的获取和释放基本分析完成。从源码分析中,我们得到:
- 写锁是独占锁,也是可重入的
- 获取写锁,必须当前系统中不存在读锁,非当前线程持有的写锁
10、读锁获取释放流程分析
读锁的获取和释放,相对于写锁的分析,要稍微复杂些。读锁是共享锁,所以在同一时间中,可以允许多个线程持有读锁。其次,写锁可以降级为读锁。
10.1、java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock#lock 获取读锁入口
public void lock() {
sync.acquireShared(1);
}
10.2、java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireShared
此方法为模板方法,tryAcquireShared 留给子类进行实现。
public final void acquireShared(int arg) {
// 参考:10.3、java.util.concurrent.locks.ReentrantReadWriteLock.Sync#tryAcquireShared
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
10.3、java.util.concurrent.locks.ReentrantReadWriteLock.Sync#tryAcquireShared 获取共享锁
获取共享锁,其内部逻辑中,大量的代码都是关于当前线程持有锁次数的统计。通过类成员变量,readHolds,cachedHoldCounter,firstReaderHoldCount 进行维护。
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 获取同步器状态值
int c = getState();
// 当前锁被写锁持有,且写锁并不是当前线程,直接返回 -1 执行 doAcquireShared(arg);
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取读锁的数量
int r = sharedCount(c);
// 判断读锁是否应该阻塞
// 1. 判断是否锁降级,参考:8.2、 final boolean apparentlyFirstQueuedIsExclusive() 方法
// 2. 读锁 < 最大获取锁数量
// 3. 通过CAS设置当前读锁的数量
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 如果当前系统中读锁的数量为 0
if (r == 0) {
// 设置当前 firstReader 为当前线程
firstReader = current;
// firstReaderHoldCount 持有读锁的数量为1
firstReaderHoldCount = 1 ;
// 如果读锁的数量不为0,且firstReader == current ,firstReaderHoldCount 累加
} else if (firstReader == current) {
firstReaderHoldCount++;
// 都不满足,说明不是第一个获取到读锁的线程
} else {
// 获取缓存持有锁的线程
HoldCounter rh = cachedHoldCounter;
// cachedHoldCounter 缓存最后一个获取读锁的线程
// 1. 判断 rh是否为 null
// 2. 判断 rh 是 rh 是否为当前线程
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
// 将当前线程设置到 readHolds 中
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 获取共享锁失败,进入循环
// 获取共享锁失败的可能性:
// 1.当前写线程降级为读线程,但是没有释放写锁
// 2.r < MAX_COUNT,超过了最大的读线程数量设置
// 3. compareAndSetState(c, c + SHARED_UNIT) 设置失败
// 参考:10.4 java.util.concurrent.locks.ReentrantReadWriteLock.Sync#fullTryAcquireShared
return fullTryAcquireShared(current);
}
10.4 java.util.concurrent.locks.ReentrantReadWriteLock.Sync#fullTryAcquireShared
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
// 如果当前锁别写锁占用,且不是当前线程,直接返回为 -1。
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)) {
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 1;
}
}
}
10.5、 java.util.concurrent.locks.AbstractQueuedSynchronizer#doAcquireShared
将共享锁添加到等待队列中
private void doAcquireShared(int arg) {
// 将当前读节点添加到队列同步器中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头结点的话
if (p == head) {
// 尝试去获取资源
int r = tryAcquireShared(arg);
// 获取读锁成功之后,进行后续节点的唤醒操作,因为读锁是共享锁
if (r >= 0) {
// 这个也是共享锁和排它锁的本质区别
// 参考10.6、 java.util.concurrent.locks.AbstractQueuedSynchronizer#setHeadAndPropagate
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 唤醒和阻塞同重入锁的逻辑一样,这里不做讨论了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
10.6、 java.util.concurrent.locks.AbstractQueuedSynchronizer#setHeadAndPropagate
// 如果是独占锁的话,如果获取锁成功,直接设置head节点,结束了
// 在共享锁中,如果获得到锁,那么他会把当前的锁给传递下去,让后续的同步器中的节点,可以唤醒
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置当前节点为head节点
setHead(node);
// 如果读锁(共享锁)获取成功,或头部节点为空,或头节点取消,或刚获取读锁的线程的下一个节点为空,
// 或在节点的下个节点也在申请读锁,则在CLH队列中传播下去唤醒线程
// 就是只要获取成功到读锁,那就要传播到下一个节点(如果一下个节点继续是读锁的申请,
// 只要成功获取,就再下一个节点,直到队列尾部或为写锁的申请,停止传播
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果下一个节点是共享锁的话,唤醒后续节点
// 如果下一个节点不是共享节点,则不进行后续唤醒操作,这里也是个闭环。
// 这样就等待前面的共享锁使用完成释放以后,唤醒后面写锁。这里十分关键。
if (s == null || s.isShared())
// 唤醒后续节点的循环逻辑,也是共享锁最核心的部分
doReleaseShared();
}
}
10.7、java.util.concurrent.locks.AbstractQueuedSynchronizer#doReleaseShared
private void doReleaseShared() {
for (;;) {
// 当前的头节点,即是已经获取到了读锁的线程
// 其实就是唤醒上面新获取到共享锁的节点的后继节点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 表示后继节点需要被唤醒
// 如果设置成功,则唤醒下一节点
// 具体的唤醒操作,和可重入锁的唤醒操作相同,这里不做表述
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 执行唤醒操作
unparkSuccessor(h);
}
// 如果后继节点暂时不需要唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果头结点没有发生变化,表示设置完成,退出循环
// 如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试
// 因为是共享锁,可能多个读线程可以获取锁
if (h == head) // loop if head changed
break;
}
}
11、结语
如果是独占锁的话,获取当前锁的时候,不会进行后续唤醒操作,等到其释放锁的时候,会唤醒后续节点。在共享锁的模式下,当前线程获取到共享锁,会判断下一个等待的节点是否是共享节点,如果是,则唤醒下一个节点。以此类推。直到下一个节点不是共享节点的时候。同时,在唤醒下一个节点的时候,可能有其他线程已经获得到了锁,重置了head节点。所以要对head节点进行重新设置。