宝沃汽车-智能系统部正在招聘Android开发工程师,感兴趣的可以发送简历到 liyilin7@borgward.com, 期待跟你成为同事!
系列文章索引
并发系列:线程锁事
- 篇一:为什么CountDownlatch能保证执行顺序?
- 篇二:并发容器为什么能实现高效并发?
- 篇三:从ReentrientLock看锁的正确使用姿势
新系列:Android11系统源码解析
- Android11源码分析:Mac环境如何下载Android源码?
- Android11源码分析:应用是如何启动的?
- Android11源码分析:Activity是怎么启动的?
- Android11源码分析:Service启动流程分析
- Android11源码分析:静态广播是如何收到通知的?
- Android11源码分析:binder是如何实现跨进程的?(创作中)
- 番外篇 - 插件化探索:插件Activity是如何启动的?
- Android11源码分析: UI到底为什么会卡顿?
- Android11源码分析:SurfaceFlinger是如何对vsync信号进行分发的?(创作中)
经典系列:Android10系统启动流程
- 源码下载及编译
- Android系统启动流程纵览
- init进程源码解析
- zygote进程源码解析
- systemServer源码解析
前言
不同的线程之间需要协作,最原始的做法就是通过等待通知机制来实现,通过Object中的wait(),notify,notifyAll()进行处理
CountDownLatch 是用来控制线程执行顺序的工具类,也可以说是处理线程协作的工具类
今天我们从CountDownLatch为切入点,看看它是如何支持线程协作的,以及内部是如何实现的
另外,我们也会列举一些其他的线程协作工具及用法
下面,正文开始!
使用CountdownLatch保证执行顺序
CountdownLatch有几个重要的函数
- 构造函数CountdownLatch(count),指定需要count个数
- countdown(),将count值进行--
- await(), 等待count值为0后进行唤醒执行
我们以四个线程t1,t2,t3,t4,分别按顺序打印a,b,c,d为例来说明CountdownLatch的使用
- 创建并启动四个线程,并创建latch对象,指定count值为4
- 在A线程内循环判断latch值是否为4,由于我们的count初始值为4,因此只要线程A处于运行状态,此条件一定是满足的,于是打印a字符,并执行latch.countDown()更新count的值为3,并通知其他线程
- 在B线程内循环判断latch值是否为3,如果条件不满足,则说明线程A还未执行,继续自旋判断;条件满足时,打印字符b,并执行latch.countDown()更新count的值为2,并通知其他线程, C,D 线程逻辑相同,此处不再赘述
- 在主线程内调用latch.await()等待四个线程执行完成(count值为0)时,通过interrupt()标志位停止线程
具体代码如下
public void latchPrint() {
CountDownLatch latch = new CountDownLatch(4);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (latch.getCount() == 4) {
System.out.println("Thread:" + "a" + " lock count:" + latch.getCount());
System.out.println("a");
latch.countDown();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (latch.getCount() == 3) {
System.out.println("Thread:" + "b" + " lock count:" + latch.getCount());
System.out.println("b");
latch.countDown();
}
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (latch.getCount() == 2) {
System.out.println("Thread:" + "c" + " lock count:" + latch.getCount());
System.out.println("c");
latch.countDown();
}
}
}
});
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (latch.getCount() == 1) {
System.out.println("Thread:" + "d" + " lock count:" + latch.getCount());
System.out.println("d");
latch.countDown();
}
}
}
});
System.out.println("latchPrint-执行开始");
t3.start();
t4.start();
t2.start();
t1.start();
try {
latch.await(); //当四个线程都执行完成后,通过设置标识位停止线程
t3.interrupt();
t4.interrupt();
t2.interrupt();
t1.interrupt();
System.out.println("latchPrint-执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
源码探究:为什么CountDownlatch能保证顺序打印?
上面的代码和逻辑中,保证顺序打印的核心在于对count值的自旋判断,及在主线程await()等待count结束后对子线程的终止
我们来看下源码中对count值是如何维护的
countdown()函数是如何实现的?
在CountDownlatch构造函数中,会创建一个Sync对象,并传入count值
这个Sync对象,即是大名鼎鼎的AQS(AbstractQueuedSynchronizer)的实现类,AQS的细节过于复杂,我们暂时先关注Sync的逻辑
Sync中的state即为我们传入的count值,这里会使用volatile保证多线程的可见性
在执行countdown()函数时,会调用AQS中的函数sync.releaseShared(1),接着调用Sync中tryReleaseShared()的具体实现
代码如下
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
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;
}
}
}
在Sync中,tryReleaseShared会在循环中获取state的值
如果state==0,说明倒数已经完成(已执行完通知线程的操作),返回false
如果state!=0, 则说明计数还未结束,使用CAS乐观锁的方式去更改state的值;如果更改后的值非0,则返回false,否则返回true,继续执行doReleaseShared()(也是AQS中的函数)
该函数中同样会有一个循环(有点类似于Android中Looper机制),其中会使用一个双向循环链表对线程Thread进行存储(volatile Thread thread)
在循环中会取出处于SIGNAL状态的Node节点,并调用unparkSuccessor(),最终又调用到了LockSupport.unpark(s.thread)(其中为native实现,我们此处不做深究), 通知其已执行结束
具体代码如下
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);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
await()函数的具体实现
根据上文分析,我们知道AQS中会维护一个双向循环链表对线程进行维护,链表的节点插入,则是在await()中执行的逻辑,其中会调用sync.acquireSharedInterruptibly(1),其中会调用CountdownLatch中Sync的tryAcquireShared()进行判断,如果state为0,则返回1,否则返回-1
AQS中会判断其返回值小于0时(即state不等于0),执行doAcquireSharedInterruptibly(),将对应的线程封装为Node节点,添加到链表中进行维护
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
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
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
到底,我们将CountdownLatch中的重要函数已经分析完毕了,下面我们来总结下
小结
CountdownLatch的保证线程顺序执行依赖于await()和countdown()两个函数
其中具体的实现是在AQS中会对await()的线程使用双向循环链表进行维护
在执行countdown()后,判断当前维护的state值是否为0,当倒数到0时,会取出链表中的节点,进行通知操作
简言之,CountdownLatch也是等待通知机制的一种实现方式,在使用上进行了封装和优化,使用CAS的方式对state进行更新,使用上更方便,也更高效
进阶思考:顺序执行的本质是什么?
除了使用CountdownLatch,我们还可以通过很多种方式来实现顺序执行的需求
- 使用原子类(AtomicInteger),实现类似于对state进行维护的功能,根据state值来进行判断是否执行对应的逻辑;
- 通过维护多把锁,使用wait(),notify()等待通知机制来进行线程的休眠和唤醒;
- 通过使用ReentrantLock,维护多个Condition,使用signal() 和await()对线程持有的锁进行休眠唤醒
- 使用公平锁,让线程按启动顺序执行
综上,不论使用何种方式,都离不开各线程之间的通知和唤醒(自旋判断相当于也是在等待对应的条件满足)
针对具体的使用场景,我们要考虑代码的可维护性,执行效率等问题,选择合适的方案进行实现
后记
本篇是并发专题文章的开篇,后面的文章将对并发上的其他问题进行进一步探讨,比如本文涉及到的AQS机制,各种锁的使用场景及优缺点,如何保证线程安全,等等
另外,Android系统源码的系列文章我也将持续输出,近期的计划包括vysc信号的分发,binder通信机制等
我是释然,我们下期文章再见啦!