目录
一、什么是AQS?
二、AQS的两种模式(共享模式与独占模式)
三、同步队列
四、独占锁和共享锁的获取释放流程
4.1 独占锁的 获取和释放流程
4.2 共享锁的 获取和释放流程
五、重入锁ReentrantLock
六、读写锁ReentrantReadWriteLock
七、闭锁CountDownLatch
谈到JUC中的锁机制,不得不说一下AQS。希望大家耐心阅读,最好能翻着源码走一走。
AQS是AbustactQueuedSynchronizer的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。
另外我们还需要了解一下什么是自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
2.1 独占模式与共享模式介绍
- 独占模式指的是一个锁如果被一个线程持有,其他线程必须等待。这也是独占模式的应用场景。 (比如:ReentrantLock、ReadWriteLock的写锁等)
- 共享模式指的是允许多个线程获取同一个锁而且可能获取成功。多个线程读取一个文件可以采用共享模式。 (比如:CountDownLatch、ReadWriteLock的读锁)
2.2 独占锁和共享锁在实现上的区别(具体实现后面会讲到)
- 独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态
- 共享锁的同步状态>1,取值由上层同步组件确定
- 独占锁队列中头节点运行完成后释放它的直接后继节点
- 共享锁队列中头节点运行完成后释放它后面的所有节点
- 共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况
不管是独占锁还是共享锁,本质上都是对AQS内部的一个变量state的获取,state是一个原子性的int变量,可用来表示锁状态、资源数等。
同步队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO(先进先出)原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点假如到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用。
节点加入同步队列:
首节点的设置:
同步队列内部是由节点Node组成,首先介绍下AQS的内部类Node,源码如下:
static final class Node {
/**
* Marker to indicate a node is waiting in shared mode
*/
static final Node SHARED = new Node();
/**
* Marker to indicate a node is waiting in exclusive mode
*/
static final Node EXCLUSIVE = null;
//取消
static final int CANCELLED = 1;
//等待触发
static final int SIGNAL = -1;
//等待条件
static final int CONDITION = -2;
//状态需要向后传播
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
Node的实现很简单,就是一个普通双向链表的实现,这里主要说明一下内部的几个等待状态:
- CANCELLED:值为1,当前节点由于超时或中断被取消。
- SIGNAL:值为-1,表示当前节点的前节点被阻塞,当前节点在release或cancel时需要执行unpark来唤醒后继节点。
- CONDITION:值为-2,当前节点正在等待Condition,这个状态在用到Condition时才会被用到。
- PROPAGATE:值为-3,(针对共享锁) releaseShared()操作需要被传递到其他节点,这个状态在doReleaseShared中被设置,用来保证后续节点可以获取共享资源。
- 0:初始状态,当前节点在sync queue中,等待获取锁。
另外,AQS已经为我们提供了同步器的基础操作,如果要自定义同步器,必须实现以下几个方法:(重点来啦)
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false。
- isHeldExclusively():该线程是否正在独占资源。只有用到Condition才需要去实现它。
4.1.1 独占锁获取锁
源码如下:
//独占模式获取锁
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
通过源码我们可以看出:内部主要调用了三个方法 tryAcquire(int),addWaiter(Node), acquireQueued(Node,int),后面会对各个方法进行详细分析,acquire(int)方法流程如下:
4.1.2 独占锁释放锁
独占模式下释放指定量的资源,成功释放后调用unparkSuccessor
唤醒head的下一个节点。
// 独占模式释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) {//尝试释放资源
Node h = head;//头结点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒head的下一个节点
return true;
}
return false;
}
通过源码我们可以看出:内部主要调用了三个方法 tryAcquire(int),addWaiter(Node), acquireQueued(Node,int),流程如下:tryRelease(int)、unparkSuccessor(Node),后面会对各个方法进行详细分析,release(int)方法流程如下:
4.2.1 共享锁获取锁
// 共享模式获取锁
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
通过源码我们可以看出:内部主要调用了两个方法,tryAcquireShared(int)、doAcquireShared(int)。其中tryAcquireShared
需要自定义同步器实现。后面会对各个方法进行详细分析。acquireShared
方法流程如下:
4.2.2 共享锁释放锁
// 共享模式释放锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();//释放锁,并唤醒后继节点
return true;
}
return false;
重入锁指的是当前线程成功获取锁后,如果再次访问该临界区,则不会对自己产生互斥行为。Java中对ReentrantLock和synchronized都是可重入锁,synchronized由jvm实现可重入即使,ReentrantLock都可重入性基于AQS实现。
同时,ReentrantLock还提供公平锁和非公平锁两种模式。
重入锁的基本原理是判断上次获取锁的线程是否为当前线程,如果是则可再次进入临界区,如果不是,则阻塞。重入锁的最主要逻辑就锁判断上次获取锁的线程是否为当前线程。
5.1 公平锁
公平锁是指当多个线程尝试获取锁时,成功获取锁的顺序与请求获取锁的顺序相同,所以 公平锁的核心就是判断同步队列中当前节点是否有前驱节点(划重点)
由于ReentrantLock是基于AQS实现的,底层通过操作同步状态来获取锁,下面看一下公平锁的实现逻辑:
/**
* tryAcquire的公平版本。 除非是递归调用或没有调用者或是第一个调用,否则不授予访问权限。
*/
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 通过AQS获取同步状态
int c = getState();
// 此处为公平锁的核心,即判断同步队列中当前节点是否有前驱节点(公平锁划重点)
// 并且如果同步状态为0,说明临界区处于无锁状态
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//将当前线程设置为锁的持有者
setExclusiveOwnerThread(current);
return true;
}
}
// 如果临界区处于锁定状态,且上次获取锁的线程为当前线程(重入锁划重点)
else if (current == getExclusiveOwnerThread()) {
// 则递增同步状态
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
5.2 非公平锁
再看一下非公平锁的实现逻辑:
非公平锁是指当锁状态为可用时,不管在当前锁上是否有其他线程在等待,新近线程都有机会抢占锁。
下面代码即为非公平锁和核心实现,可以看到只要同步状态为0,任何调用lock的线程都有可能获取到锁,而不是按照锁请求的FIFO(先进先出)原则来进行的。
/**
* 执行非公平的tryLock。 tryAcquire是在子类中实现的,但两者都需要tryf方法的非公平尝试。
*/
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 通过AQS获取同步状态
int c = getState();
// 如果同步状态为0,说明临界区处于无锁状态
if (c == 0) {
// 修改同步状态,即加锁
if (compareAndSetState(0, acquires)) {
//将当前线程设置为锁的持有者
setExclusiveOwnerThread(current);
return true;
}
}
// 如果临界区处于锁定状态,且上次获取锁的线程为当前线程(重入锁划重点)
else if (current == getExclusiveOwnerThread()) {
// 则递增同步状态
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
从上面的代码中可以看出,公平锁与非公平锁的区别仅在于是否判断当前节点是否存在前驱节点!hasQueuedPredecessors() &&,由AQS可知,如果当前线程获取锁失败就会被加入到AQS同步队列中,那么,如果同步队列中的节点存在前驱节点,也就表明存在的前驱结点线程比当前节点线程更早的获取锁,故只有等待前面的线程释放锁后才能获取锁。
Java提供了一个基于AQS到读写锁实现ReentrantReadWriteLock,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁。
6.1 读锁的获取与释放
读锁是一个共享锁,先来看源码:ReadLock.java
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
//持有的AQS对象
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
//获取共享锁
public void lock() {
sync.acquireShared(1);
}
//获取共享锁(响应中断)
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//尝试获取共享锁
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
//释放锁
public void unlock() {
sync.releaseShared(1);
}
//新建条件
public Condition newCondition() {
throw new UnsupportedOperationException();
}
public String toString() {
int r = sync.getReadLockCount();
return super.toString() +
"[Read locks = " + r + "]";
}
}
6.1.1 读锁的获取
读锁的获取lock
方法调用了同步器sync
的acquireShared
,acquireShared,这俩在前面共享锁,独占锁
已经分析过。这里主要介绍一下tryAcquireShared
在ReentrantReadWriteLock中的实现。 tryAcquireShared(int) 源码如下:
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 通过AQS获取同步状态
int c = getState();
// 持有写锁的线程可以获取读锁,如果获取锁的线程不是current线程;则返回-1。
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取读锁数量
int r = sharedCount(c);
// 检查时候有资格获取读锁
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 首次获取读锁,初始化firstReader和firstReaderHoldCount
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 当前线程是首个获取读锁的线程
firstReaderHoldCount++;
} else { // 更新cachedHoldCounter
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;
}
return fullTryAcquireShared(current); // 下一步会讲到
}
读锁获取步骤解释如下:
- 如果“写锁”已经被持有,这时候可以继续获取读锁,但如果持有写锁的线程不是当前线程,直接返回-1(表示获取失败);
- 如果在尝试获取锁时不需要阻塞等待(由公平性决定),并且读锁的共享计数小于最大数量
MAX_COUNT
,则直接通过CAS函数更新读取锁的共享计数,最后将当前线程获取读锁的次数+1。- 如果第二步执行失败,则调用
fullTryAcquireShared
尝试获取读锁(接下来讲)。
获取步fullTryAcquireShared源码如下:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {//自旋
int c = getState();
//持有写锁的线程可以获取读锁,如果获取锁的线程不是current线程;则返回-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();//当前线程持有读锁数为0,移除计数器
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)//超出最大读锁数量
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {//CAS更新读锁数量
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;
}
}
}
6.1.2 读锁的释放
读锁的释放unlock
方法调用了同步器sync
的releaseShared
,releaseShared。这俩在前面共享锁,独占锁
已经分析过。这里主要介绍一下tryReleaseShared
在ReentrantReadWriteLock中的实现。tryReleaseShared(int)源码如下:
protected final boolean tryReleaseShared(int unused) {
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;
}
for (;;) {//自旋
int c = getState();
int nextc = c - SHARED_UNIT;//获取剩余资源/锁
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
读锁释放 步骤解释如下:
- 更新当前线程计数器的锁计数;
- CAS更新释放锁之后的state,这里使用了自旋,在state争用的时候保证了CAS的成功执行。
6.2 写锁的获取与释放
6.2.1 写锁的获取
写锁是一个独占锁,所以我们看一下ReentrantReadWriteLock.WriteLock中tryAcquire(int)的实现:
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
Thread current = Thread.currentThread();
// 通过AQS获取同步状态
int c = getState();
// 获取同步状态
int w = exclusiveCount(c);
// 如果同步状态不为0,说明存在读锁或写锁
if (c != 0) {
// 如果存在读锁(c !=0 && w == 0),则不能获取写锁(保证写对读的可见性)
// 如果当前线程不是上次获取写锁的线程,则不能获取写锁(写锁为独占锁)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// exclusiveCount(acquires) 在低16为写锁同步状态上利用CAS进行修改(增加写锁同步状态,实现可重入)
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 将当前线程设置为写锁的获取线程
setExclusiveOwnerThread(current);
return true;
}
6.2.2 写锁的释放
写锁的释放过程与独占锁基本相同:在释放的过程中,不断减少读锁同步状态,只有当同步状态为0时,写锁完全释放。
/*
* 请注意,条件可以调用tryRelease和tryAcquire。
* 因此,它们的参数可能包含在条件等待期间释放的读取和写入保持,并在tryAcquire中重新建立。
*/
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;
}
CountDownLatch 是一个并发构造,它允许一个或多个线程等待一系列指定操作的完成。
有时候会有这样的需求,多个线程同时工作,然后其中几个可以随意并发执行,但有一个线程需要等其他线程工作结束后,才能开始。举个例子,开启多个线程分块下载一个大文件,每个线程只下载固定的一截,最后由另外一个线程来拼接所有的分段,那么这时候我们可以考虑使用CountDownLatch来控制并发。
CountDownLatch 以一个给定的数量初始化。countDown()每被调用一次,这一数量就减一。通过调用 await()方法之一,线程可以阻塞等待这一数量到达零。
先来看看CountDownLatch的构造方法:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
构造方法会传入一个整型数count,之后调用CountDownLatch的countDown0()方法会对count减一,直到count减到0的时候,调用await()方法的线程继续执行。
下面列出构造方法的基本用法:
public static final int requestTotal = 20;// 总的请求个数
CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
CountDownLatch的方法不是很多,先将它们一个个列举出来:
- await() :调用该方法的线程等到构造方法传入的count减到0的时候,才能继续往下执行;
- await(long timeout, TimeUnit unit):与上面的await方法功能一致,只不过这里有了时间限制,调用该方法的线程等到指定的timeout时间后,不管count是否减至为0,都会继续往下执行;
- countDown():使CountDownLatch初始值count减1;
- long getCount():获取当前CountDownLatch维护的值;
下面来看些CountDownLatch的一些重要方法。
7.1 await()方法源码:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//---------------------------我是分割线------------------------------------
//AQS中acquireSharedInterruptibly(1)的实现
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
//---------------------------我是分割线------------------------------------
//tryAcquireShared在CountDownLatch中的实现
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
wait()的实现很简单,内部调用了共享锁的获取,就是通过对资源state剩余量(state==0 ? 1 : -1)来判断是否获取到锁。在前面我们讲到过,获取共享锁时tryAcquireShared函数规定了它的返回值类型:成功获取并且还有可用资源返回正数;成功获取但是没有可用资源时返回0;获取资源失败返回一个负数。也就是说,只要state!=0,线程就进入等待队列阻塞。
7.2 countDown()方法源码:
public void countDown() {
sync.releaseShared(1);
}
//---------------------------我是分割线------------------------------------
//AQS中releaseShared(1)的实现
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();//唤醒后续节点
return true;
}
return false;
}
//---------------------------我是分割线------------------------------------
//tryReleaseShared在CountDownLatch中的实现
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
countDown() 也是很好理解的,内部调用了共享锁的释放,如果释放资源后state==0,说明已经到达latch,此时就可以调用doReleaseShared唤醒等待的线程。
通过分析源码我们可以看出CountDownLatch其实是最简单的同步类实现。它内部调用完全依赖了AQS,只要能理解了AQS,那么理解CountDownLatch就是水到渠成的事了。
关于一些锁的练习,已上传至GitHub,感兴趣的童鞋可以去看看:https://github.com/higminteam/practice/tree/master/src/main/java/com/practice/lock
其他关于JUC源码解析文章:
深入了解 Java JUC(一)之 atomic(原子数据类的包)
深入了解 Java JUC(二)之 从JUC锁机制AQS到重入锁、读写锁和CountDownLatch
java JUC 之 四种常用线程池 + Spring提供的线程池技术
参考文章:
https://www.jianshu.com/p/af9c0f404a93
https://blog.csdn.net/zhangdong2012/article/details/79983404