强烈建议先读一下参考文章,基本上说的很清楚了。
Lock模型
一些前置知识:
- unsafe提供的两个关键机制
- CAS,CPU提供的原子指令
- park/unpark, 调用这个native函数会将线程调度和停止调度
- unsafe不能被直接调用,可被虚拟机代码调用-> LockSupport封装了这两个机制。
AbstractQueueSychronizer中间类,继承这个类可以实现各种EOF场景(EOF- 异常控制流程)
AQS(AbstractQuenedSynchronizer抽象队列式同步器)
- 核心思想&基本框架
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS维护了一个volatile语义(支持多线程下的可见性)的共享资源变量state和一个FIFO线程等待队列CLH(多线程竞争state被阻塞时会进入此队列)。
理解AbstractQueueSychronizer的关键 - 数据结构,使用的最简单的双向链表 - 操作系统底层,就是上面介绍的LockSupport封装 - 控制逻辑,主要是通过state和Node中的waitState控制的
state
共享资源变量state,三种访问方式:
- getState()
- setState(int newState)
- compareAndSetState(int expect, int update)
资源共享的方式,两种:
- 独占式(Exclusive) 只有单个线程能够成功获取资源并执行,如ReentrantLock。
- 共享式(Shared) 多个线程可成功获取资源并执行,如Semaphore/CountDownLatch等。
Node节点 - CLH(Craig, Landin, and Hagersten locks)
CLH锁其实就是一种是基于逻辑队列非线程饥饿的一种自旋公平锁(基于单向链表(隐式创建)的高性能、公平的自旋锁),由于是 Craig、Landin 和 Hagersten三位大佬的发明,因此命名为CLH锁。 AQS内部的FIFO线程等待队列,通过内部类Node来实现
static final class Node {
// 表明节点在共享模式下等待的标记
static final Node SHARED = new Node();
// 表明节点在独占模式下等待的标记
static final Node EXCLUSIVE = null;
// 表征等待线程已取消的
static final int CANCELLED = 1;
// 表征需要唤醒后续线程
static final int SIGNAL = -1;
// 表征线程正在等待触发条件(condition)
static final int CONDITION = -2;
// 表征下一个acquireShared应无条件传播
static final int PROPAGATE = -3;
/**
* SIGNAL: 当前节点释放state或者取消后,将通知后续节点竞争state。
* CANCELLED: 线程因timeout和interrupt而放弃竞争state,当前节点将与state彻底拜拜
* CONDITION: 表征当前节点处于条件队列中,它将不能用作同步队列节点,直到其waitStatus被重置为0
* PROPAGATE: 表征下一个acquireShared应无条件传播
* 0: None of the above
*/
volatile int waitStatus;
// 前继节点
volatile Node prev;
// 后继节点
volatile Node next;
// 持有的线程
volatile Thread thread;
// 链接下一个等待条件触发的节点
Node nextWaiter;
// 返回节点是否处于Shared状态下
final boolean isShared() {
return nextWaiter == SHARED;
}
// 返回前继节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// Shared模式下的Node构造函数
Node() {
}
// 用于addWaiter
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// 用于Condition
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
题外话:来做个手动实现锁的小练习吧 简单的非公平自旋锁以及基于排队的公平自旋锁的实现 blog.csdn.net/dm_vincent/… CLH锁的原理和实现 blog.csdn.net/dm_vincent/…
实现
下面通过AbstractQuenedSynchronizer(同步器)和ReentrantLock(锁)来详细说明一下AQS的原理。除了共享和独占的特点外,重点关注它的几个特性:
- 公平和非公平
- 超时可中断
- 条件中断
API
同步器是实现锁的关键,利用同步器将锁的语义实现,然后在锁的实现中聚合同步器。 可以这样理解:
- 锁的API是面向使用者的,它定义了与锁交互的公共行为,而每个锁需要完成特定的操作也是透过这些行为来完成的(比如:可以允许两个线程进行加锁,排除两个以上的线程),但是实现是依托给同步器来完成
- 同步器面向的是线程访问和资源控制,它定义了线程对资源是否能够获取以及线程的排队等操作。
锁和同步器很好的隔离了二者所需要关注的领域,严格意义上讲,同步器可以适用于除了锁以外的其他同步设施上(包括锁)
AQS内部定义(实现)的方法
独占式
- acquire(int)
- acquireInterruptibly(int)
- tryAcquireNanos(int,long)
- release(int)
共享式
- acquireShared(int)
- acquireSharedInterruptibly(int)
- tryAcquireSharedNanos(int,long)
- releaseShared(int)
需要继承的锁自定义实现的方法
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
AQS需要子类复写的方法均没有声明为abstract,目的是避免子类需要强制性覆写多个方法,因为一般自定义同步器要么是独占方法,要么是共享方法,只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。当然,AQS也支持子类同时实现独占和共享两种模式,如ReentrantReadWriteLock。
另外可以看到,一般try开头的都是需要锁实现的,但是tryAcquireNanos方法例外,它的作用使用实现可超时中断的锁
源码分析
独占锁的实现
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述逻辑主要包括:
- 尝试获取(调用tryAcquire更改状态,需要保证原子性); 在tryAcquire方法中使用了同步器提供的对state操作的方法,利用compareAndSet保证只有一个线程能够对状态进行成功修改,而没有成功修改的线程将进入sync队列排队。
- 如果获取不到,将当前线程构造成节点Node并加入sync队列; 进入队列的每个线程都是一个节点Node,从而形成了一个双向队列,类似CLH队列,这样做的目的是线程间的通信会被限制在较小规模(也就是两个节点左右)。
- 再次尝试获取(acquireQueued),如果没有获取到那么将当前线程从线程调度器上摘下,进入等待状态。
tryAcquire在ReentrantLock中的实现是在FairSync中
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
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;
}
//对于非公平锁也一样
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
addWaiter
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
上述逻辑主要包括:
- 使用当前线程构造Node; 对于一个节点需要做的是将当节点前驱节点指向尾节点(current.prev = tail),尾节点指向它(tail = current),原有的尾节点的后继节点指向它(t.next = current)。
- 先行尝试在队尾添加; 如果尾节点已经有了,然后做如下操作: (1)分配引用T指向尾节点; (2)将节点的前驱节点更新为尾节点(current.prev = tail); (3)如果尾节点是T,那么将当尾节点设置为该节点(tail = current,原子更新); (4)T的后继节点指向当前节点(T.next = current)。 注意第3点是要求原子的。 这样可以以最短路径O(1)的效果来完成线程入队,是最大化减少开销的一种方式。
- 如果队尾添加失败或者是第一个入队的节点。 如果是第1个节点,也就是sync队列没有初始化,那么会进入到enq这个方法,进入的线程可能有多个,或者说在addWaiter中没有成功入队的线程都将进入enq这个方法。 可以看到enq的逻辑是确保进入的Node都会有机会顺序的添加到sync队列中,而加入的步骤如下: (1)如果尾节点为空,那么原子化的分配一个头节点,并将尾节点指向头节点,这一步是初始化; (2)然后是重复在addWaiter中做的工作,但是在一个while(true)的循环中,直到当前节点入队为止。 进入sync队列之后,接下来就是要进行锁的获取,或者说是访问控制了,只有一个线程能够在同一时刻继续的运行,而其他的进入等待状态。而每个线程都是一个独立的个体,它们自省的观察,当条件满足的时候(自己的前驱是头结点并且原子性的获取了状态),那么这个线程能够继续运行。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
上述逻辑主要包括:
- 获取当前节点的前驱节点; 需要获取当前节点的前驱节点,而头结点所对应的含义是当前站有锁且正在运行。
- 当前驱节点是头结点并且能够获取状态,代表该当前节点占有锁; 如果满足上述条件,那么代表能够占有锁,根据节点对锁占有的含义,设置头结点为当前节点。
- 否则进入等待状态。 如果没有轮到当前节点运行,那么将当前线程从线程调度器上摘下,也就是进入等待状态。
小总结
- 状态的维护; 需要在锁定时,需要维护一个状态(int类型),而对状态的操作是原子和非阻塞的,通过同步器提供的对状态访问的方法对状态进行操纵,并且利用compareAndSet来确保原子性的修改。
- 状态的获取; 一旦成功的修改了状态,当前线程或者说节点,就被设置为头节点。
- sync队列的维护。
在获取资源未果的过程中条件不符合的情况下(不该自己,前驱节点不是头节点或者没有获取到资源)进入睡眠状态,停止线程调度器对当前节点线程的调度。
这时引入的一个释放的问题,也就是说使睡眠中的Node或者说线程获得通知的关键,就是前驱节点的通知,而这一个过程就是释放,释放会通知它的后继节点从睡眠中返回准备运行。
锁的释放
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
上述逻辑主要包括:
- 尝试释放状态; tryRelease能够保证原子化的将状态设置回去,当然需要使用compareAndSet来保证。如果释放状态成功过之后,将会进入后继节点的唤醒过程。
- 唤醒当前节点的后继节点所包含的线程。
tryRelease在ReentranLock-Sync中的实现
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
通过LockSupport的unpark方法将休眠中的线程唤醒,让其继续acquire状态。
private void unparkSuccessor(Node node) {
// 将状态设置为同步状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取当前节点的后继节点,如果满足状态,那么进行唤醒操作
// 如果没有满足状态,从尾部开始找寻符合要求的节点并将其唤醒
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
上述逻辑主要包括,该方法取出了当前节点的next引用,然后对其线程(Node)进行了唤醒,这时就只有一个或合理个数的线程被唤醒,被唤醒的线程继续进行对资源的获取与争夺。 回顾整个资源的获取和释放过程: 在获取时,维护了一个sync队列,每个节点都是一个线程在进行自旋,而依据就是自己是否是首节点的后继并且能够获取资源; 在释放时,仅仅需要将资源还回去,然后通知一下后继节点并将其唤醒。 这里需要注意,队列的维护(首节点的更换)是依靠消费者(获取时)来完成的,也就是说在满足了自旋退出的条件时的一刻,这个节点就会被设置成为首节点。
共享锁
简单来讲,读锁和读锁是可以共享的,其它情况,读锁和写锁,写锁和读锁、写锁和写锁,都是互斥的。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里acquireShared()的流程就是:
-
tryAcquireShared()尝试获取资源,成功则直接返回;
-
失败则通过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) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
int r = tryAcquireShared(arg);//尝试获取资源
if (r >= 0) {//成功
setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC
if (interrupted)//如果等待过程中被打断过,此时将中断补上。
selfInterrupt();
failed = false;
return;
}
}
//判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);//head指向自己
//如果还有剩余量,继续唤醒下一个邻居线程
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
跟acquireQueued()很相似,流程并没有太大区别。只不过这里将补中断的selfInterrupt()放到doAcquireShared()里了,而独占模式是放到acquireQueued()之外.
跟独占模式比,还有一点需要注意的是,这里只有线程是head.next时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。,假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。老大先唤醒老二,老二一看资源不够,他是把资源让给老三呢,还是不让?答案是否定的!老二会继续park()等待其他线程释放资源,也更不会去唤醒老三和老四了。独占模式,同一时刻只有一个线程去执行,这样做未尝不可;但共享模式下,多个线程是可以同时执行的,现在因为老二的资源需求量大,而把后面量小的老三和老四也都卡住了。当然,这并不是问题,只是AQS保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
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);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
翻译一下注释:
确保发布传播,即使有其他正在进行中的获取/发布。如果需要信号,这将以通常的方式试图打开头部的后继者。但如果没有,则状态设置为PROPAGATE,以确保在发布时继续传播。此外,我们必须进行循环,以防在此过程中添加了新节点。此外,与unpark后继的其他用法不同,我们需要知道CAS重置状态是否失败,如果失败就要重新检查。
就是说通过循环等待的方式设置头结点,如果是SIGNAL待唤醒的情况,则唤醒,如果是ws=0的状态,则设置为PROPAGATE,让acquiredShared的时候可以继续传播。
跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。
小总结
未完待续
- 可中断(public final void acquireInterruptibly(int arg))
- 超时控制 (private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException)
- 公平锁
- 条件中断