阅读 858

【线程锁事】篇一:为什么CountDownlatch能保证执行顺序?

系列文章索引

并发系列:线程锁事

  1. 篇一:为什么CountDownlatch能保证执行顺序?

  2. 篇二:并发容器为什么能实现高效并发?

  3. 篇三:从ReentrientLock看锁的正确使用姿势

新系列:Android11系统源码解析

  1. Android11源码分析:Mac环境如何下载Android源码?

  2. Android11源码分析:应用是如何启动的?

  3. Android11源码分析:Activity是怎么启动的?

  4. Android11源码分析:Service启动流程分析

  5. Android11源码分析:静态广播是如何收到通知的?

  6. Android11源码分析:binder是如何实现跨进程的?(创作中)

  7. 番外篇 - 插件化探索:插件Activity是如何启动的?

  8. Android11源码分析: UI到底为什么会卡顿?

  9. Android11源码分析:SurfaceFlinger是如何对vsync信号进行分发的?(创作中)

经典系列:Android10系统启动流程

  1. 源码下载及编译

  2. Android系统启动流程纵览

  3. init进程源码解析

  4. zygote进程源码解析

  5. 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的使用

  1. 创建并启动四个线程,并创建latch对象,指定count值为4

  2. 在A线程内循环判断latch值是否为4,由于我们的count初始值为4,因此只要线程A处于运行状态,此条件一定是满足的,于是打印a字符,并执行latch.countDown()更新count的值为3,并通知其他线程

  3. 在B线程内循环判断latch值是否为3,如果条件不满足,则说明线程A还未执行,继续自旋判断;条件满足时,打印字符b,并执行latch.countDown()更新count的值为2,并通知其他线程, C,D 线程逻辑相同,此处不再赘述

  4. 在主线程内调用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),其中会调用CountdownLatchSynctryAcquireShared()进行判断,如果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,我们还可以通过很多种方式来实现顺序执行的需求

  1. 使用原子类(AtomicInteger),实现类似于对state进行维护的功能,根据state值来进行判断是否执行对应的逻辑;

  2. 通过维护多把锁,使用wait()notify()等待通知机制来进行线程的休眠和唤醒;

  3. 通过使用ReentrantLock,维护多个Condition,使用signal()await()对线程持有的锁进行休眠唤醒

  4. 使用公平锁,让线程按启动顺序执行

综上,不论使用何种方式,都离不开各线程之间的通知和唤醒(自旋判断相当于也是在等待对应的条件满足)

针对具体的使用场景,我们要考虑代码的可维护性,执行效率等问题,选择合适的方案进行实现

后记

本篇是并发专题文章的开篇,后面的文章将对并发上的其他问题进行进一步探讨,比如本文涉及到的AQS机制,各种锁的使用场景及优缺点,如何保证线程安全,等等

另外,Android系统源码的系列文章我也将持续输出,近期的计划包括vysc信号的分发,binder通信机制等

我是释然,我们下期文章再见啦!

如果本文对你有所启发,请多多点赞支持

文章分类
Android
文章标签