系列文章索引
并发系列:线程锁事
新系列:Android11系统源码解析
-
Android11源码分析:binder是如何实现跨进程的?(创作中)
-
Android11源码分析:SurfaceFlinger是如何对vsync信号进行分发的?(创作中)
经典系列:Android10系统启动流程
前言
不同的线程之间需要协作,最原始的做法就是通过等待通知机制来实现,通过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
通信机制等
我是释然,我们下期文章再见啦!