释放锁
Release/TryRelease
unlock操作实际上调用了AQS的release操作,释放持有的锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
释放锁看看,成功说明锁确实被当前线程拥有,然后看AQS队列头节点是否为空,能否被唤醒,可以的话就唤醒继任节点。
对于独占锁java.util.concurrent.locks.ReentrantLock.Sync.tryRelease(int)展示了如何尝试释放锁操作
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;
}
- 判断是否当前线程,不是就抛出异常,因为一个线程不能释放另一个线程持有的锁,否则执行2
- 将AQS状态位减去要释放的次数,独占锁总是1,如果剩余状态位0(就是没有线程持有锁)
当前线程是最后一个持有锁的线程,清空AQS独占现场执行 - 剩余的状态位写会AQS,没有线程持有锁就返回true,否则false
tryRelease成功后,release才能检查是否需要唤醒下一个继任节点,前提是AQS队列头节点需要锁(waitStatus!=0),如果头节点需要锁,检查下一个继任节点是否需要锁。
以前说acquireQueued拿到锁,将当前持有锁的节点设为头节点,如头节点释放锁,需要寻找头节点下一个需要锁继任节点,并唤醒。
private void unparkSuccessor(Node node) {
//此时node是需要是需要释放锁的头结点
//清空头结点的waitStatus,也就是不再需要锁了
compareAndSetWaitStatus(node, Node.SIGNAL, 0);
//从头结点的下一个节点开始寻找继任节点,当且仅当继任节点的waitStatus<=0才是有效继任节点,否则将这些waitStatus>0(也就是CANCELLED的节点)从AQS队列中剔除
//这里并没有从head->tail开始寻找,而是从tail->head寻找最后一个有效节点。
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);
}
Condition
大部分为了解决Object.wait/notify/notifyAll难以使用的问题,条件便于在转台条件为true的另一个线程通知他前,挂起线程让他等待。
看看Condition接口定义的方法:
void await() throws InterruptedException; void awaitUninterruptibly(); long awaitNanos(long nanosTimeout) throws InterruptedException; boolean await(long time, TimeUnit unit) throws InterruptedException; boolean awaitUntil(Date deadline) throws InterruptedException; void signal(); 对应Object.notify void signalAll(); Object.notifyAll
await挂起线程,一旦满足条件就会唤醒,再次获取锁。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
一个 Condition可以在多个地方被await,需要一个FIFO结构将这些Condition串联,根据需要唤醒一个或多个,通常是所有,所以Condition内部需要一个FIFO队列。
signal/signalAll
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
Node p = enq(node);
int c = p.waitStatus;
if (c > 0 || !compareAndSetWaitStatus(p, c, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
signal就是唤醒Condition队列中第一个非Cancelled线程
闭锁(Latch)
一种同步方法,可以延迟线程进度直到线程到达某状态,大门打开之后,所有线程通过。门状态也不能关上了。
CountDownLatch是闭锁的实现,有个计数器,countDown方法对计数器做减操作,await方法等待计数器为0,所有await线程都会阻塞直到计数器为0
CoutndownLatch
public void await() throws InterruptedException
public boolean await(long timeout, TimeUnit unit) throws InterruptedException
public void countDown()
public long getCount()
getCount是获取当前计数
package xylz.study.concurrency.lock;
import java.util.concurrent.CountDownLatch;
public class PerformanceTestTool {
public long timecost(final int times, final Runnable task) throws InterruptedException {
if (times <= 0) throw new IllegalArgumentException();
final CountDownLatch startLatch = new CountDownLatch(1);
final CountDownLatch overLatch = new CountDownLatch(times);
for (int i = 0; i < times; i++) {
new Thread(new Runnable() {
public void run() {
try {
startLatch.await();
//
task.run();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
} finally {
overLatch.countDown();
}
}
}).start();
}
//
long start = System.nanoTime();
startLatch.countDown();
overLatch.await();
return System.nanoTime() - start;
}
}
两个闭锁,一个startLatch,另一个overLatch,工作准备完后就调用startLatch.countDown打开闭锁,线程执行,第二个闭锁在所有任务执行完后主线程才能继续。第二个闭锁,初始化了一个计数器,每个任务执行完成后就减一,计数器变0,主线程闭锁overLatch拿到信号就可以继续执行。相当于把任务拆分成N份,每一份独立完成任务。
那么看看CountDownLatch是如何实现await方法的内部直接调用了AQS的acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
AQS的tryAcquireShared实现如下,对于闭锁,总应该是1或者-1
public int tryAcquireShared(int acquires) {
return getState() == 0? 1 : -1;
}
state的值就是初始化的count
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
break;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
// Arrive here only if interrupted
cancelAcquire(node);
throw new InterruptedException();
}
1.线程节点以共享模式加入AQS的CLH队列中,进行2
2.检查当前节点的前任节点,如果是头结点并且当前闭锁计数大于0,将前节点设置为头结点,唤醒继任节点,返回结束线程阻塞,否则3
3.检查是否应该阻塞,该就阻塞,直到被唤醒,重复2
4.2、3有异常就抛出,结束线程阻塞
有一点要注意一下,这个设置头结点并唤醒继任节点setHeadAndPropagate,由于前面的tryAcquireShared总返回1或者-1,进入setHeadAndPropagate时总是有propagate>=0,所以propagate==1
private void setHeadAndPropagate(Node node, int propagate) {
setHead(node);
if (propagate > 0 && node.waitStatus != 0) {
Node s = node.next;
if (s == null || s.isShared())
unparkSuccessor(node);
}
}
unparkSuccessor方法用于唤醒队列中最前面的节点
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//找到下一个需要唤醒的结点s
if (s == null || s.waitStatus > 0) {//如果为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
那么waitStatus的状态如下
- CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。 0:新结点入队时的默认状态。
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
tryReleaseShared正是采用CAS操作减少计数
public boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
接下来讲CyclicBarrier,如果说CountDownLatch是一次性的,那么CyclicBarrier正好可以循环使用,它允许一组线程相互等待,直到一组任务执行完毕。
package xylz.study.concurrency.lock;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
final CyclicBarrier barrier;
final int MAX_TASK;
public CyclicBarrierDemo(int cnt) {
barrier = new CyclicBarrier(cnt + 1);
MAX_TASK = cnt;
}
public void doWork(final Runnable work) {
new Thread() {
public void run() {
work.run();
try {
int index = barrier.await();
doWithIndex(index);
} catch (InterruptedException e) {
return;
} catch (BrokenBarrierException e) {
return;
}
}
}.start();
}
private void doWithIndex(int index) {
if (index == MAX_TASK / 3) {
System.out.println("Left 30%.");
} else if (index == MAX_TASK / 2) {
System.out.println("Left 50%");
} else if (index == 0) {
System.out.println("run over");
}
}
public void waitForNext() {
try {
doWithIndex(barrier.await());
} catch (InterruptedException e) {
return;
} catch (BrokenBarrierException e) {
return;
}
}
public static void main(String[] args) {
final int count = 10;
CyclicBarrierDemo demo = new CyclicBarrierDemo(count);
for (int i = 0; i < 100; i++) {
demo.doWork(new Runnable() {
public void run() {
//do something
try {
Thread.sleep(1000L);
} catch (Exception e) {
return;
}
}
});
if ((i + 1) % count == 0) {
demo.waitForNext();
}
}
}
}
一共100个任务,每10个一起处理,仅当上一组处理完才能进行下一组。CyclicBarrierDemo构建了count+1任务组,每一个任务执行完毕要等待同组其他任务执行后继续,在剩下50%,30% 0执行其他任务,CyclicBarrierDemo创建了11个任务组,其中有一个任务是为了外界方便挂起主线程。每一个子任务,代码中的await从尾部10开始,执行到0为止,如果有一个被中断,那就会抛出一场InterruptedException执行一次挂一次线程,await返回执行完的索引,索引从任务-1开始的,CyclicBarrier可循坏,每一组完毕,执行下一组。有一个未执行完毕的话,那就唤醒其他线程呗。
CyclicBarrier的API如下:
- public CyclicBarrier(int parties) 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。
- public CyclicBarrier(int parties, Runnable barrierAction) 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
- public int await() throws InterruptedException, BrokenBarrierException 在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。
- public int await(long timeout,TimeUnit unit) throws InterruptedException, BrokenBarrierException,TimeoutException 在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。
- public int getNumberWaiting() 返回当前在屏障处等待的参与者数目。此方法主要用于调试和断言。
- public int getParties() 返回要求启动此 barrier 的参与者数目。
- public boolean isBroken() 查询此屏障是否处于损坏状态。
- public void reset() 将屏障重置为其初始状态。
接下来是CyclicBarrier.await()的实现片段
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
剖析一下,原子性意味着要有中断的,这个状态变量就是broken,由于有竞争资源的存在(index/broken)要加锁lock之后变为这样了
- 检查是否有中断为broken,存在抛出异常,否则进行2
- 检查当前线程是否中断,是就设置中断位,让其他进入等待的线程知道,唤醒正在等待的线程,以InterruptedException返回,表示线程处理中断,否则3
- 剩余任务-1,如果剩0,达到终点,执行屏障点任务,创建新Generation,这个过程会唤醒其他所有线程,但其概念现场是屏障点线程,其他线程就在等待。否则4
- 到4了就说明没到屏障,这个线程就应该park,for循坏要park线程,采用Condition.await(),也就是trip.await(),为何Condition,因为await等待一个条件,条件满足应该都被唤醒,Condition正好满足。所以3中达到屏障点创建Generation一定要唤醒其他线程。
生成 下一个循坏周期并唤醒其他执行完并等待的线程,count做索引计数,表示有多少线程要执行。isBroken描述的是 generation.broken,就是线程组是否发生异常。设置这个是因为如果一个屏障点发生异常,即使其他线程被唤醒也会循坏等待。没有线程来唤醒这些线程。
private void nextGeneration() {
trip.signalAll();
count = parties;
generation = new Generation();
}
CyclicBarrier还有一个reset方法手动立即将所有线程中断,恢复屏障点,进行下一组任务执行。与重新创建新屏障点相比,维护代价小一些。具体看博客博客讲解