上一篇文章已经讲完了大致了解同步器对于我们的作用,这里就来分析下如何完成线程同步。主要包括:同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据结构与模版方法。
同步器依赖内部的同步队列(一个FIFO的双向队列)来完成同步状态的管理。同步器包含着两个节点类型的引用,一个指向头节点、一个指向尾节点。
当前线程获取同步状态失败时,同步器会将当前线程及等待状态信息构造成一个节点,将其插入同步队列的尾部(基于CAS的设置尾节点方法),同时会阻塞该线程。
首节点时获取同步状态成功的节点,当首节点的线程同步状态释放时,会把首节点中的后继节点中的线程唤醒,使其再次尝试获取同步状态。而后继节点在获取同步状态成功时将自己设置为首节点(由于只有一个线程获取同步状态,所以这里就不需要CAS)。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.调用自定义同步器实现的tryAcquire方法,获取同步状态
2.若获取同步状态不成功则调用addWaiter方法将该节点加到同步队列的尾部,最后调用acquireQueued()使该节点自旋获取同步状态。
3.如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
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;
}
//enq
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;
}
}
}
}
addWaiter方法通过compareAndSetTail()方法CAS把节点加入到等待队列的队尾,成功并返回当前线程所在的结点。否则,通过enq(node)方法初始化一个等待队列
enq(node)
用于将当前节点插入等待队列,如果队列为空,则初始化当前队列。整个过程以CAS自旋的方式进行,直到成功加入队尾为止。
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;
}
//是否应该被park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
队列中的线程自旋地以独占且不可中断的方式获取同步状态(acquire),直到拿到锁之后再返回。该方法的实现分成两部分:
如果当前节点已经成为头结点,尝试获取锁(tryAcquire)成功,然后返回;
否则检查当前节点是否应该被park,然后将该线程park并且检查当前线程是否被可以被中断。
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;
}
这里需要弥补一下一些知识点:Node节点的属性----waitStatus(int)
CANCELLED:1,在同步队列中等待的线程等待超时或者中断,需要从同步队列中取消等待。
SIGNAL:-1,后继节点的线程处于等待,而当前节点的线程如果释放同步状态或者取消,将会通知后继节点,表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
CONDITION:-2,节点在等待队列中,节点线程在等待condition上,当其他线程对Condition调用signal()后,会把该节点从等待队列转移到同步队列中,加入到对同步状态的获取中
PROPAGATE:-3,表示下一次共享式同步状态获取将会无条件地被传播下去
INITIAL:0,初始化状态
shouldParkAfterFailedAcquire()对当前节点的前一个节点的状态进行判断,对当前节点做出不同的操作.
1、例如if (ws == Node.SIGNAL),前节点的waitStatus是Node.SIGNAL,表明前置节点已经知道了释放同步后会通知后继节点,,这时候当前节点应当停止自旋parkAndCheckInterrupt()
2、如果ws>0代表示是CANCELLED,这类节点直接清除出同步队列。
3、else剩下的就是前置节点还不是Node.SIGNAL,就是不知道释放同步状态后还要通知后置节点,则会尝试将前置节点状态位修改为Node.SIGNAL。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
该方法让线程去休息,真正进入等待状态。park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
- 结点进入队尾后,检查状态,让前置节点知道后面还有节点,让其释放同步状态的时候通知自己;
- 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
- 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。
- 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
- 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
通过唤醒后继节点,unparkSuccessor()通过unpark来唤醒处于等待状态的线程。
在获取同步状态的时候,同步器维护一个同步队列,获取同步状态失败的线程都会被加入到队列中进行自旋(并不是一直自旋,而是会停下线程进去wait状态),移除队列的条件是前置节点为头节点并且成功获取到了同步状态,在释放同步状态的时候调用tryRelease()方法释放同步状态,然后唤醒头节点的后置节点。
上图基本已经让我们了解了基本的共享式锁与独占式锁之间的关系了。最典型的例子就是:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里acquireShared()的流程就是:
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);
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);
}
}
此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。
跟独占式其实差不多,不一样的就在于通过tryAcquireShared大于0,来判断是否有资源。
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();
}
}
此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//尝试释放资源
doReleaseShared();//唤醒后继结点
return true;
}
return false;
}
先释放掉资源后,唤醒后继。