并发编程 · 基础篇(下) · 三大分析法分析线程池

小木箱成长营并发编程系列教程:

并发编程 · 基础篇(上) · android 线程那些事

并发编程 · 基础篇(中) · 三大分析法分析 Handler

并发编程 · 提高篇(上) · Java 并发关键字那些事

并发编程 · 提高篇(下) · Java 锁安全性那些事

并发编程 · 高级篇(上) · Java 内存模型那些事

并发编程 · 高级篇(下) · Java 并发 BATJ 面试之谈

并发编程 · 实战篇 · android 下载器实现

Tips: 关注微信公众号小木箱成长营,回复 "并发编程" 可获得并发编程免费思维导图

一、序言

Hello,我是小木箱,欢迎来到小木箱成长营并发编程系列教程,今天将分享并发编程 · 基础篇(下) · 三大分析法分析线程池

三大分析法分析 android 线程池主要分为四部分,第一部分是 4W2H 分析线程池,第二部分是 MECE 分析线程池,第三部分是 SCQA 分析线程池,最后一部分是结语。

其中,4W2H 分析线程池主要围绕线程池提出了 6 个高价值问题。

其中,MECE 分析线程池主要分为线程池基本操作、线程池生命周期、线程池工作原理、线程池代码案例分析、线程池的性能优化、线程池注意事项、线程池线程数量确定和线程池业务防劣化 8 部分。

最后,以 SCQA 的形式在 B 站上投放一些来自 BATJD 等大厂的高频面试题。

image.png

如果完全掌握小木箱成长营并发编程系列教程,那么任何人都能通过高并发相关的技术面试。

二、4W2H 分析线程池

2.1 What: 线程池具体定义

alt

线程池是一种管理和调度线程的机制,线程池可以控制线程的数量,确保线程有效地工作,并且可以重复使用以前创建的线程,从而减少系统的开销。

2.2 Why: 线程池使用原因

alt

如果不使用线程池,每个任务都新开一个线程处理 for 循环创建线程,开销太大,我们希望有固定数量的线程,来执行这 1000 个线程,就避免了反复创建并销毁线程所带来的开销问题

2.3 How: 线程池使用方式

2.3.1 线程池 API 文档

API简介
isShutdown判断当前线程的状态是否是 SHUTDOWN,即是否调用了 shutdown 方法
isTerminating当前线程池的状态是否小于 TERMINATED,并且大于等于 SHUTDOWN,即当前线程是否已经 shutdown 并且正在终止。
isTerminated线程池是否终止完成
awaitTermination等待直到线程状态变为 TERMINATED
finalize重新 Object 的方法,当线程池对象被回收的时候调用,在这里调用 shutdown 方法终止线程,防止出现内存泄漏
prestartCoreThread预先启动一个核心线程
prestartAllCoreThreads预先启动所有的核心线程
remove从任务队列中移除指定任务
purge从队列中移除所有的以及取消的 Future 任务
getPoolSize获取线程池中线程的数量,即 Worker 的数量
getActiveCount获取线程池中正在执行任务的线程 Worker 数量
getLargestPoolSize获取线程池曾经开启过的最大的线程数量
getTaskCount获取总的任务数量,该值为每个 Worker 以及完成的任务数量,以及正在执行的任务数量和队列中的任务数量
getCompletedTaskCount获取 Worker 以及执行的任务数量
beforeExecute任务执行前调用
afterExecute任务执行后调用
terminated线程终止时调用,用来回收资源

2.3.2 线程池基础结构

线程池的基础结构分为三部分: 阻塞队列 BlockingQueue、核心参数和 Worker 工作线程。

2.3.2.1 阻塞队列

线程池 ThreadLocal 是一个阻塞队列 BlockingQueue

private final BlockingQueue<Runnable> workQueue;

阻塞队列 BlockingQueue 主要是用来提供任务缓冲区的功能,工作线程从阻塞队列 BlockingQueue 中取出任务来执行。

线程池中存放任务用的是 offer 方法,取出任务用的是 poll 方法。 阻塞队列 BlockingQueue 有三种通用策略

直接提交

直接提交,当一个线程提交一个任务的时候,如果没有一个对应的线程来取任务,提交阻塞或者失败。同样的当一个线程取任务的时候,如果没有一个对应的线程来提交任务,取阻塞或者失败。

SynchronousQueue 就是这种队列的实现,这种队列通常要求 maximumPoolSizes 最大线程数量是无界的,避免提交的任务因为 offer 失败而被拒绝执行。

当提交任务的速率大于任务执行的速率的时候,这种队列会导致线程数量无限的增长。

无界队列

无界队列,无界队列实现的例子是 LinkedBlockingQueue,当核心线程都处于忙碌的情况的时候, 提交的任务都会添加到无界队列中,不会有超过核心线程数 corePoolSize 的线程被创建。

这种队列可能适用于任务之间都是独立的,任务的执行都是互不影响的。

例如,在一个 web 服务器中,这种队列能够用来使短时间大量的并发请求变得更加平滑,当提交任务的速率大于任务执行的速率的时候,这种队列会导致队列空间无限增长。

有界队列

有界队列,有界队列实现的例子是 ArrayBlockingQueue,使用该队列配合设置有限的 maximumPoolSizes 可以防止资源耗尽,这种情况下协调和控制资源和吞吐量是比较困难的。

队列大小和 maximumPoolSize 的设置是比较矛盾的:使用容量大的队列和较少的线程资源会减少 CPU、OS 资源、线程上下文切换等的消耗,但是会降低系统吞吐量。

如果任务频繁的阻塞,例如任务是 IO 密集的类型,这种情况下系统能够调度更多的线程。使用小容量队列,就要要求 maximumPoolSize 大一些,从而让 CPU 保持忙碌的状态,但是可能出现线程上下文切换频繁、线程数量过多调度困难已经创建线程 OOM 导致资源耗尽的情况,从而降低吞吐量。

SynchronousQueue vs LinkedBlockingQueue vs ArrayBlockingQueue

SynchronousQueue

SynchronousQueue 适用于请求响应要求无延迟,请求 并发 量较少的场景

当线程 Worker 没有从队列取任务的时候,offer 返回 false,直接开启线程。当 Worker 从队列取任务的时候,该任务可以直接提交给 Worker 执行。

因此,这种线程池不会出现等待的情况,响应速度很快。

队列的缺点是提交任务速度大于任务执行速度时,会导致线程无限增长,因此,使用场景需要是并发量较少的情况。

例如,在 OkHttp 框架中执行 HTTP 请求就是用的这种队列构建的线程池。

LinkedBlockingQueue

LinkedBlockingQueue 适用于并发量高,任务之间都是独立的,互不影响的场景。

比如在 web 服务器中,面对瞬时大量请求的涌入,可以更加平滑的处理,从而起到削峰的作用,并且防止线程资源的耗尽。

ArrayBlockingQueue

ArrayBlockingQueue 是介于前两者之间的队列,可以协调系统资源和吞吐量之间的平衡。

2.3.2.2 核心参数

一个 Worker 代表一个工作线程,wrokers 存储的是线程池中所有的工作线程。

工作线程的核心参数有如下

private final HashSet<Worker> workers = new HashSet<Worker>();

// ---------------------------------------------------------------

//当前工作线程的数量,每创建一个Wroker,该值就加1, 可以通过getLargestPoolSize获取
private int largestPoolSize;
//完成的任务的数量
private long completedTaskCount;

// ---------------------------------------------------------------

private volatile ThreadFactory threadFactory;
private volatile RejectedExecutionHandler handler;
private volatile long keepAliveTime;
private volatile boolean allowCoreThreadTimeOut;
private volatile int corePoolSize;
private volatile int maximumPoolSize;

这几个变量都是用户设置的参数变量,为了保证参数设置的可见性,所有参数都使用volatile修饰。 ThreadFactory 是线程创建工厂,提供线程创建和配置的接口,这里使用的是工厂方法模式,默认的实现是 DefaultThreadFactory。

static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }
    //注意,Runnable r 就是工作线程接口Worker,需要传到Thread中。
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group,r,
                               namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

默认的线程工厂创建的线程名称为pool-poolNumber-thread-threadNumber,第一个线程池第一个线程名称为 pool-0-thread-0,第二个线程名称为 pool-0-thread-1,第二个线程池第一个线程名称为 pool-1-thread-0,第二个线程名称为 pool-1-thread-1,依次类推。

RejectedExecutionHandler

是当任务 被拒绝时的执行接口,提供了 4 种实现策略,默认采取 AbortPolicy 策略,如果不设置,线程池在拒绝任务的时候会抛出异常。

CallerRunsPolicy

在当前提交线程直接运行该任务。

AbortPolicy

直接抛出 RejectedExecutionException 异常。

DiscardPolicy

丢弃该任务,什么都不做。

DiscardOldestPolicy

从队列中移除头节点任务,然后再次提交任务到线程池。

keepAliveTime

闲置线程等待任务的超时时间,线程使用时间当线程数量超过 corePoolSize 的时候或者设置了允许核心线程超时的时候,否则线程会一直等待直到有新的任务。

allowCoreThreadTimeOut

允许核心线程超时,默认是 false,如果设置为 true,则核心线程会使用超时时间来等待任务。

corePoolSize

核心线程数量,默认情况下核心线程会一直等待直到有新的任务,如果设置了允许核心线程超时,则最小线程数为 0。

maximumPoolSize

可以开启的最大的线程数量。

2.3.2.3 工作线程
2.3.2.1.1 Worker 定义

Worker 代表一个工作线程,该类实现了 Runnable 接口,继承自 AQS,内部实现了一套简单的锁机制。这里使用的是代理模式,Worker 实现了 Runnable,然后轮训任务队列,取出任务执行。详细代码如下:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable
{
    /** Thread this worker is running in.  Null if factory fails. */
    //代表工作线程
    final Thread thread;
    /** Initial task to run.  Possibly null. */
    //该线程执行的第一个任务
    Runnable firstTask;
    /** Per-thread task counter */
    //该线程已经执行的任务数量
    volatile long completedTasks;
    /**
     * Creates with given first task and thread from ThreadFactory.
     * @param firstTask the first task (null if none)
     */
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        //注意 将当前代理Runnable传递到ThreadFactory作为线程的执行载体。
        this.thread = getThreadFactory().newThread(this);
    }
    /** Delegates main run loop to outer runWorker  */
    //该Runnable作为代理,开启轮训,从队列中取出提交的Runnable来执行。
    public void run() {
        runWorker(this);
    }
    // Lock methods
    // The value 0 represents the unlocked state.
    // The value 1 represents the locked state.
    //简单的互斥锁,把0修改为1代表获取到锁,把1修改为0代表释放锁。
    protected boolean isHeldExclusively() {
        return getState() != 0;
    }
    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0,1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }
    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }
}
2.3.2.1.2 Worker 创建

那么该如何创建一个工作线程呢?

创建工作线程主要分为四步:

  1. 判断当前线程的数量是否超过了 corePoolSize 或者 maximumPoolSize,如果超过返回 false,如果没有,通过 cas 增加当前线程的数量。
  2. 创建 Worker,在其构造方法中会通过 ThreadFactory 创建线程,然后将 Worker 添加到集合中。
  3. 如果 Worker 创建成功,调用 Thread 的 start 方法启动线程,开启任务轮训。
  4. 如果线程启动失败,处理 Worker 创建失败的情况,将 Worker 移除,避免内存泄漏,然后尝试终止线程池。
private boolean addWorker(Runnable firstTask,boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        //
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            //获取当前线程的数量,如果是核心线程模式,线程数量不能大于corePoolSize
            //如果是非核心线程模式,线程数量不能大于maximumPoolSize 否则返回false
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //使用cas 更新线程Worker的数量,更新成功退出循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        //创建Worker,Worker构造函数中会通过ThreadFactory创建Thread
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            //加锁,并发安全控制
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    //线程已经启动 抛出异常
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    //添加到 HashSet<Worker>集合中
                    workers.add(w);
                    //更新当前线程数量 给largestPoolSize赋值
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                //启动线程 开启任务轮训
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        //如果线程启动失败 会将刚刚添加的Worker移除
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}
//Worker添加失败的处理
 private void addWorkerFailed(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //移除Worker 防止内存泄漏
            if (w != null)
                workers.remove(w);
            //减少Worker的数量
            decrementWorkerCount();
            //尝试终止线程池
            tryTerminate();
        } finally {
            mainLock.unlock();
        }
    }
2.3.2.1.3 Worker 线程原理

Worker 线程原理分为七步:

  1. 开启循环,先执行 firstTask,然后调用 getTask()从队列中取出任务执行。
  2. 任务执行前会先判断线程池的状态,当线程池的状态是 STOP 的时候,中断任务线程。
  3. 调用 beforeExecute方法。
  4. 调用任务的 run 方法执行任务。
  5. 调用 afterExecute方法。
  6. 任务执行完成后,累加当前 Worker 执行的任务数量到 Wroker 的 completedTasks变量中。
  7. 循环结束后,线程执行结束,处理后续情况。
 //Worker的run方法中调用runWorker方法开启轮训
public void run() {
            runWorker(this);
 }
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        //首先执行firstTask 然后在通过getTask()方法从队列中取出任务执行。
        while (task != null || (task = getTask()) != null) {
            //执行任务前先加锁 可以通过tryLock方法 判断当前Worker线程是否在执行任务
            //如果在执行任务,tryLock返回false,否则,返回true
            w.lock();
            // If pool is stopping,ensure thread is interrupted;
            // if not,ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            //如果线程池是处于STOP的状态,即调用了shutDownNow方法,确保线程是中断的
            if ((runStateAtLeast(ctl.get(),STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(),STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                //hook方法,任务执行前调用beforeExecute,任务执行后调用afterExecute
                //可以通过这两个方法来监控线程池中任务的执行情况
                beforeExecute(wt,task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task,thrown);
                }
            } finally {
                task = null;
                //累加当前Worker已经完成的任务数量
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        //Worker退出时的后续处理
        processWorkerExit(w,completedAbruptly);
    }
}
2.3.2.1.3 Task 任务的获取

任务获取主要分为两步:

  1. 线程池并没有区分核心线程和非核心线程,仅仅保证核心线程的数量。 当线程数量大于核心线程数量,或者设置了核心线程可超时,则通过超时 polll 方法获取任务,否则通过无限阻塞 take 方法获取任务。,线程数量等于核心线程数量时,剩下的线程会一直阻塞直到有任务执行,线程数量大于核心线程数量是,非核心线程会在超时时间之后退出。刚开始创建的核心线程可能会退出,后来创建的非核心线程可能会一直存活到最后。
  2. 当线程池的状态是 STOP或者线程池的状态是 SHUTDOWN并且队列是空的时候,会返回 null,Wroker 线程结束执行,减少 Worker 的数量。
private Runnable getTask() {
    boolean timedOut = false// Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        //返回null的情况有两种
        //1.线程池的状态变为STOP。
        //2.线程池的状态是SHUTDOWN,当时队列是空的。
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        int wc = workerCountOf(c);
        // Are workers subject to culling?
        //线程池并没有区分核心线程和非核心线程,只是根据当前的线程数量来使用不同的获取任务的方法
        //1.线程数量大于corePoolSize 或者设置了核心线程超时,则使用超时poll方法获取任务
        //2.线程数量等于corePoolSize并且没有设置核心线程超时,使用take方法获取任务
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            //超时,当前的Worker即将退出循环,因此,修改Worker的数量,然后返回null。
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS) :
                workQueue.take();
            //如果不是空的返回,如果是空的,说明已经超时,设置timeOut为true
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
2.3.2.1.4 Worker 线程退出
  1. 正常退出情况下,在 getTask 方法中已经调整了线程数量,但是异常退出情况,来不及调整,在这里需要重新调整线程数量。
  2. 移除 Worker,统计总任务数量。
  3. 尝试终止线程池,调用 tryTerminate()方法。
  4. 如果当前线程状态小于 STOP,即 RUNNING 和 SHUTDOWN 状态,需要补齐线程数量。如果线程异常退出,直接调用 addWorker 方法补齐线程;如果线程正常退出,判断当前线程数量是否小于线程池最小线程数量,如果小于,直接补齐,否则,直接返回。正常退出可能是超过核心线程数量的线程获取 任务超时了,这种情况是不需要补齐的。如果最小线程数量为 0,但是队列中还有任务,线程池的状态不是 STOP,是需要补齐的。
private void processWorkerExit(Worker w,boolean completedAbruptly) {
    //completedAbruptly 代表线程是正常退出 还是异常退出
    //如果线程是正常退出,在getTask方法中已经调整了workerCount
    //如果线程异常退出,需要在这里调整workerCount
    if (completedAbruptly) // If abrupt,then workerCount wasn't adjusted
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        //把该Worker执行的任务数量累加到总任务数量变量中 然后从集合中移除Worker
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }
    //尝试终止线程池,只有最后一个Worker线程执行完,才会终止线程池
    tryTerminate();
    //获取线程状态,如果线程池的状态小于STOP 即RUNNING和SHUTDOWN状态,
    //并且线程是正常退出,计算当前应该存活的最小线程数量,如果min为0,但是队列不是空的,
    //则线程池还需要线程来执行任务,修改min为1
    //如果当前线程数量大于min,则直接返回,不需要补齐线程空缺
    //如果当前线程数量小于min,则调用addWorker补齐线程空缺。
    int c = ctl.get();
    if (runStateLessThan(c,STOP)) {

        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        addWorker(null,false);
    }
}
 //尝试终止线程池

 final void tryTerminate() {
        for (;;) {
            int c = ctl.get();
           //1.当前线程状态为RUNNING,直接返回,此时未调用shutDown或者shutDownNow方法,不需要终止。
           //2.当前线程状态大于TIDYING,说明其他Worker已经开始执行terminated()方法,为了保证该法仅            //  执行1次,直接返回。
           //3.当前线程状态为SHUTDOWN并且队列不是空的,直接返回,需要等待队列任务执行完再终止。
            if (isRunning(c) ||
                runStateAtLeast(c,TIDYING) ||
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
                return;
            //如果worker数量不为0,尝试中断当前闲置的线程,即在poll和take中等待的线程,从而让所有线程
            //都退出任务轮训,加速线程池回收进程。
            if (workerCountOf(c) != 0) { // Eligible to terminate
                interruptIdleWorkers(ONLY_ONE);
                return;
            }

            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                //更新线程池的状态为TIDYING,调用terminated(),这是一个hook方法,
                //可以在这里面做一些资源回收的操作,执行完后,设置线程池状态为TERMINATED
                //唤醒在awaitTermination方法上等待的线程。
                if (ctl.compareAndSet(c,ctlOf(TIDYING,0))) {
                    try {
                        terminated();
                    } finally {
                        ctl.set(ctlOf(TERMINATED,0));
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }
//尝试中断等待在取出任务的线程,如果onlyOnw为true,只会中断一个。
private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }
2.3.2.1.5 Worker 线程终止
  1. 线程池的状态为 RUNNING,直接返回,不需要终止。
  2. 线程状态为 SHUTDOWN 并且队列不是空的,直接返回,需要等待队列任务执行完再终止。
  3. 当前线程状态大于 TIDYING,说明其他 Worker 已经开始执行 terminated()方法,为了保证该方法仅执行一次,直接返回。
  4. 如果 worker 数量不为 0,尝试中断当前闲置的线程,即在 poll 和 take 中等待的线程,从而让所有线程都退出任务轮训,加速线程池回收进程。
  5. 更新线程池的状态为 TIDYING,调用 terminated(),这是一个 hook 方法,可以在这里面做一些资源回收的操作,执行完后,设置线程池状态为 TERMINATED,唤醒在 awaitTermination 方法上等待的线程。

2.3.3 创建和停止线程池

alt

ExecutorService executorService=Executors.newFixedThreadPool(5);
       // 1.创建线程池
        for(int i=0;i< 10;i++){

           executorService.execute(new Runnable(){

          @Override
          public void run(){
             System.out.println(Thread.currentThread().getName()+"办理业务");
}
        });
    }
// 2.关闭线程池
        executorService.shutdown();
    // executorService.shutdownNow();
       }
    }

2.3.4 手动创建 vs 自动创建

  • 一般情况下,应该手动创建线程池,因为手动创建可以更好地控制线程池的大小,以及线程池中线程的生命周期。
  • 自动创建的线程池大小可能不够,导致线程饥饿,或者线程池中线程的生命周期可能太长,导致系统资源浪费。

2.3.5 线程数量配置

一般来说,线程数量的设定要取决于任务的复杂度和计算机的性能。

如果任务比较复杂,那么线程数量可以设定的比较多,可以提高程序的并行处理能力,从而提高效率。

但是,如果线程数量设定的太多,可能会导致系统资源利用率过高,从而降低系统的效率。

因此,线程数量的设定应根据任务的复杂度和计算机的性能来合理设定。

2.3.6 停止线程池

image.png

使用shutdown()方法来停止线程池,shutdown()方法会等待线程池中正在执行的任务完成,然后才会停止线程池。

如果需要立即停止线程池,可以使用 shutdownNow()方法,shutdownNow()方法会尝试终止正在执行的任务,并且拒绝接收新的任务。

2.3.7 线程池状态

alt
System.out.println(executorService.isShutdown());
System.out.println(executorService.isTerminated());

//关闭线程池

executorService.shutdown();
System.out.println(executorService.isShutdown());
System.out.println(executorService.isTerminated());

输出结果:

false

false

true

true

线程池的状态有五种,分别是 RUNNING、SHUTDOWN、STOP、TIDYING 和 TERMINATED。

线程池把线程数量和线程状态打包到了一个 int 变量中,然后使用 AtomicInteger 原子类来修改和获取该值。一方面可以节约内存,一方面仅仅使用一个原子类就可以操作两个变量,减少了存取数据的复杂性。 线程池的 5 个状态值存储到高 3 位中,线程数量存储到底 29 位中。runStateOf方法是用来获取高 3 位的线程池状态值的,workerCountOf是用来获取低 29 位的线程池中的线程数量的,ctlOf是把两个值通过或运算打包到一个值中。



private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING,0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs,int wc) { return rs | wc; }

RUNNING 是运行状态,接受新任务,处理队列中的任务。

SHUTDOWN 是关闭状态, 不接受新任务,但是处理队列中的任务。

STOP 是停止状态,不接受新任务,不处理队列中的任务,中断正在执行任务的线程。

TIDYING 是整理状态, 所有的任务已经被终止,所有的线程已经执行完变为 TERMINATED 状态,workerCount 为 0,线程池之后会调用 terminated()扩展 hook 方法,最后变为 TERMINATED 状态。

TERMINATED 是终止状态,terminated()方法执行完成,在该方法可以做一些资源回收的工作,此时的线程池队列清空,线程终结,资源回收完毕。

线程池的状态是不可逆的,一旦进入 TERMINATED 状态,便无法重置,必须重新创建一个新的线程池才能提交任务,和线程的使用是一样的。

2.3.8 线程池状态切换

那么,线程池如何进行状态切换呢?

RUNNING / SHUTDOWN

调用 shutdown()方法,或者隐式的在 finalize()方法中调用, 线程池的状态变为 RUNNING 或 SHUTDOWN

RUNNING / SHUTDOWN -> STOP

调用 shutdownNow(), 线程池的状态由 SHUTDOWN 变成 STOP

SHUTDOWN -> TIDYING

队列是空的,线程数量是 0,任务都执行完毕, 线程池的状态由 SHUTDOWN 变成 TIDYING

STOP -> TIDYING

线程数量为 0, 线程池的状态由 STOP 变成 TIDYING

TIDYING -> TERMINATED

terminated() 执行完成时候, 线程池的状态由 TIDYING 变成 TERMINATED

TERMINATED

awaitTermination() 调用该方法会一直阻塞直到线程池的状态变成 TERMINATED

2.4 When: 线程池使用时机

alt
  1. 短时间任务:如果需要在应用程序中执行多个短期任务,那么使用线程池可以提高效率并降低资源消耗。
  2. 多用户请求:如果应用程序需要处理多个用户请求,而每个请求需要执行耗时的操作,那么使用线程池可以让应用程序更好地响应用户请求。
  3. 并发访问:如果多个线程需要访问共享资源,例如数据库或文件系统,那么使用线程池可以避免线程之间的竞争条件,并提高应用程序的吞吐量。
  4. 异步任务:如果应用程序需要执行异步任务,例如下载文件或处理大量数据,那么使用线程池可以让应用程序更加高效地执行这些任务,并且避免阻塞主线程。

2.5 How Much: 线程池业务价值

alt

因为线程存在两个弊端.第一个是反复创建线程开销大,第二个是过多的线程会占用太多内存

解决过多的线程会占用太多内存的思路是让这部分线程都保持工作,且可以反复执行任务避免生命周期的损耗

解决过多的线程会占用太多内存的思路是用少量的线程避免内存占用过多

而线程池刚好契合了上述两个优势,而且线程池有以下三个业务价值:

第一个是复用线程,减少线程频繁创建和销毁带来的系统开销。加快响应速度;

第二个是合理利用 CPU 和内存;

第三个是统一的线程管理,避免出现随意开启线程导致线程数量过多从而引发 OOM。

2.6 Where: 线程池使用场景

alt

在 Android 开发中,线程池常常被用于以下场景:

  1. 处理网络请求:Android 应用通常需要与服务器进行数据交互,网络请求通常是一个异步操作,使用线程池可以避免网络请求阻塞 UI 线程,保持应用的响应性。
  2. 处理耗时操作:例如对大文件的读写、复制、压缩等操作,操作会阻塞 UI 线程,导致应用卡顿,使用线程池可以将操作放到工作线程中执行。
  3. 并行执行多个任务:当需要同时执行多个任务时,例如下载多个文件,使用线程池可以使任务并行执行,提高效率。
  4. 处理定时任务:当需要执行定时任务时,例如轮询服务器,定时更新 UI 等,使用线程池可以在定时任务完成后将结果返回到 UI 线程中。
  5. 处理大量任务队列:例如使用 RecyclerView 展示大量数据,需要异步加载图片等操作,使用线程池可以管理任务队列,优化系统资源的使用。

综上所述,线程池在 Android 开发中的应用场景主要是处理网络请求、处理耗时操作、并行执行多个任务、处理定时任务以及处理大量任务队列等,能够提高应用的性能和响应速度,同时避免 UI 线程阻塞和 ANR 问题。

三、MECE 分析线程池

3.1 线程池的基本操作

alt

线程池的基本操作包括:

3.1.1 创建线程池

使用 ThreadPoolExecutor 类的构造函数创建线程池。ThreadPoolExecutor 是创建线程池的工具类,封装了几种常用线程池的创建方法,常用方法有如下几种:

固定线程线程池(FixedThreadPool)
固定线程线程池定义

线程池中的线程数量固定不变,当有任务提交时,如果线程池中有空闲线程,则立即使用空闲线程执行任务;如果没有,则等待有线程空闲为止。

固定线程线程池原理分析

创建线程数量固定的线程池,核心线程数 corePoolSize 和最大线程数 maximumPoolSize 相同,核心线程不超时,队列是 LinkedBlockingQueue,队列大小没有限制。

固定线程线程池使用场景

适合数据数量固定的数据处理场景,例如百度网盘中的批量文件下载功能,指定五个线程同时下载文件,其余任务都在队列排队。

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
固定线程线程池项目应用

照片导出功能,把 10000 个人员照片加密后导出到 U 盘里,使用线程池数量为 5 的固定线程池执行任务,还有一个任务分派线程,主要负责查询数据库,监控线程池和提交任务。

缓存线程池(CachedThreadPool)
缓存线程池定义

线程池中的线程数量可以根据任务的多少自动调整,如果有大量任务提交,则线程池会动态增加线程数量;如果没有任务提交,则线程池会动态减少线程数量。

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
缓存线程池原理分析

创建缓存线程池,核心线程数 corePoolSize 为 0,最大线程数 maximumPoolSize 是 Integer.MAX_VALUE,线程超时时间是 60s,队列是 SynchronousQueue 同步队列。

缓存线程池使用场景

适合对响应速度要求高,并发少的场景,Okhttp 就是用的缓存线程池来处理 http 请求的,符合手机上并发请求少,响应速度快的要求。

缓存线程池项目应用

使用 Okhttp 的过程间接使用来了缓存线程池,项目中应该谨慎使用该线程池。

定时器线程池(ScheduledThreadPool)

可以在固定的时间间隔或者指定的时间执行任务,该线程池可以设置固定的线程数量或者可变的线程数量。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
单线程池(SingleThreadExecutor)
单线程线程池定义

线程池中只有一个线程,所有任务都在同一个线程中按照队列顺序依次执行。

单线程线程池原理分析

创建单线程线程池,核心线程数 corePoolSize 和最大线程数 maximumPoolSize 都是 1,核心线程不超时,队列是 LinkedBlockingQueue,队列大小没有限制。

单线程线程池使用场景

适合任务并发少,触发频繁,任务执行时间固定的业务场景。

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
单线程线程池项目应用

人脸识别成功保存记录是通过一个单线程线程池保存记录的,人脸识别的间隔时间大于记录保存时间,因此正常情况下任务不会出现阻塞在队列的情况。

ForkJoin 线程池

该线程池是 Java 7 引入的一种专门用于处理分治算法的线程池,可以递归地将任务拆分成小任务,并将小任务分配给线程池中的线程执行,然后将小任务的结果合并起来,最终得到大任务的结果。

ForkJoinPool forkJoinPool = new ForkJoinPool();

3.1.2 提交任务

提交任务方式

使用 execute、submit 方法提交任务到线程池的任务队列中。

fixedThreadPool.execute(new Runnable() {
    public void run() {
        // 执行任务
    }
});

cachedThreadPool.execute(new Runnable() {
    public void run() {
        // 执行任务
    }
});

scheduledThreadPool.schedule(new Runnable() {
    public void run() {
        // 执行任务
    }
}, 5, TimeUnit.SECONDS);

singleThreadExecutor.execute(new Runnable() {
    public void run() {
        // 执行任务
    }
});

forkJoinPool.invoke(new RecursiveTask() {
    public Object compute() {
        // 执行任务
    }
});

线程池的 execute() 方法和 submit() 方法都用于向线程池提交任务,但是它们有以下几个区别:

  1. 返回值不同: execute() 方法没有返回值,而 submit() 方法返回一个 Future 对象。
  2. 异常处理不同: execute() 方法没有办法处理任务执行时抛出的异常,而 submit() 方法可以使用返回的 Future 对象处理任务执行时抛出的异常。
  3. 任务类型不同: execute() 方法只能提交 Runnable 类型的任务,而 submit() 方法可以提交 RunnableCallable 类型的任务。
  4. 方法重载: execute() 方法只有一种重载形式,而 submit() 方法有多种重载形式,可以指定返回结果、延迟执行等参数。

因此,当需要获取任务执行结果或者处理任务执行时可能会抛出的异常时,应该使用 submit() 方法;

当不需要获取任务执行结果或者不需要处理任务执行时可能会抛出的异常时,可以使用 execute() 方法。 execute() 方法源码如下:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
     /*
      * 1.如果当前线程数量小于核心线程数,开启一个新线程,然后把任务作为该线程首个任务来执行
      * 2.如果当前线程数量等于核心线程数,尝试添加任务到阻塞队列BlockingQueue
      * 3.如果添加队列失败,即队列已满,开启一个新线程,然后把任务作为该线程首个任务来执行
      * 4.如果第3步开启线程失败,即线程数量超过最大线程数,调用RejectedExecutionHandler的       *       *   rejectedExecution方法执行拒绝策略。
      */
    //获取线程池状态和线程数量的组合值,这两个值被打包到了一个int中
    int c = ctl.get();
    //workerCountOf 获取Worker的数量,即线程数量。
    //isRunning  获取线程池的状态,判断线程池是否是运行状态
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(commandtrue))
            return;
        //重新获取,多线程环境,该值可能已经发生变化
        c = ctl.get();
    }
    //尝试添加到队列
    if (isRunning(c) && workQueue.offer(command)) {
        //重新检查状态值 如果线程已经shutdown 则拒绝添加任务
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null,false);
    }
    //以非核心线程添加模式创建线程,如果失败,走拒绝策略
    else if (!addWorker(commandfalse))
        reject(command);
}

3.1.3 关闭线程池

  1. shutdown

使用 shutdown 方法关闭线程池,等待任务队列中的任务执行完毕后再关闭。

调用 shutdown 方法后的状态转换:RUNNING-->SHUTDOWN-->TIDYING-->TERMINATED

  1. shutdownNow

使用 shutdownNow 方法立即关闭线程池,会中断正在执行的任务,并返回未执行的任务列表。

3.1.4 动态修改线程池大小

使用 setCorePoolSize 和 setMaximumPoolSize 方法动态修改线程池中的线程数量。

调用 shutdownNow 方法后的状态转换:RUNNING-->STOP-->TIDYING-->TERMINATED

3.1.5 拒绝策略

当任务队列已满并且线程池中的线程数量已达到最大值时,使用 setRejectedExecutionHandler 方法设置拒绝策略来处理无法处理的任务。

3.1.6 监控线程池

使用 getActiveCount、getCompletedTaskCount、getTaskCount 等方法获取线程池的状态信息。

基本操作可以满足大多数线程池的需求,同时 Java 线程池还提供了很多高级特性,例如定时任务、线程池工厂等,可以根据具体需求进行选择。

3.2 线程池的生命周期

alt

线程池的生命周期通常包括以下阶段:

  1. 创建:线程池被创建,但还没有开始处理任务。
  2. 启动:线程池被启动,开始接受任务,并且根据配置参数创建指定数量的线程。
  3. 运行:线程池正在运行中,等待接收任务并且执行。
  4. 终止:线程池被终止,所有的任务已经被执行完毕,线程池中的所有线程被销毁。

需要注意的是,在线程池被终止之前,可能会存在一些情况导致线程池被关闭,比如程序发生异常、线程池被主动关闭等情况。此时,线程池中的所有任务可能无法全部被执行完毕,因此在实际使用中需要注意线程池的关闭策略,避免出现数据丢失等问题。

3.3 线程池的工作原理

线程池是一种多线程处理的机制,线程池允许在应用程序中预先创建一定数量的线程并将它们放在一个池中,线程可以重复使用,以减少线程的创建和销毁开销。

一句话总结线程池工作原理: 线程池的实现整体流程是一个可配置的生产者消费者模型,然后基于单一的阻塞缓冲队列来实现的

线程池工作原理分为六个步骤讲解,第一个步骤是初始化线程池,第二个步骤是将任务添加到任务队列,第三个步骤是检查线程池状态,第四个步骤是取出任务并执行,第五个步骤是处理任务异常,第六个步骤是关闭线程池。

  1. 初始化线程池

创建一个线程池对象,并设置线程池的参数,如线程池的大小、任务队列的大小、线程的优先级等。

参考 #3.1.1 四种线程池的创建方式

  1. 将任务添加到任务队列

当有任务需要执行时,将任务添加到任务队列中。

参考 #2.3.2.1.2 Worker 创建

  1. 检查线程池状态

线程池会周期性地检查自身状态,如果当前线程池中的线程数小于预设的最小线程数,则会创建新的线程。

参考 #2.3.2.1.3 Worker 线程原理

  1. 取出任务并执行

线程池中的线程会不断从任务队列中取出任务并执行。

参考 #2.3.2.1.4 Task 任务的获取

alt

  1. 如果当前线程数量小于核心线程数,开启新线程,将当前任务做为新线程的第一个任务来执行。
  2. 如果当前线程数量等于核心线程数,尝试添加任务到队列。
  3. 如果添加队列失败,即队列是满的,则以开启新线程,将当前任务做为新线程的第一个任务来执行。
  4. 如果新线程开启失败,即当前线程数量等于最大线程数量,执行拒绝策略。
  1. 处理任务异常

如果任务执行过程中发生了异常,线程池可以处理异常并记录异常信息。

参考 #3.1.5 拒绝策略

  1. 关闭线程池

当线程池不再需要使用时,需要将线程池关闭。关闭线程池时,首先需要将任务队列中的任务执行完毕,然后再将线程池中的线程关闭。

参考 #3.1.3 关闭线程池

通过使用线程池,可以优化系统性能,减少线程创建和销毁的开销,避免线程过多导致系统资源不足的情况,并提高系统的可维护性和可扩展性。

3.4 线程池代码案例分析

3.4.1 OkHttp

在 OKHttp 中 AsyncCall、Dispatcher 和 ConnectionPool 都是通过线程池进行维护的。

alt

AsyncCall

AsyncCall 是一个 Runnable 接口,可以通过线程池异步执行,下面是 run 方法

异步请求的执行流程:

  1. 使用 AsyncTimeout 监听请求是否超时,会开启一个子线程,线程名称 Okio Watchdog ,超时后会调用 Call 的 cancel 取消请求 。AsyncTimeout 监听的是 http 请求的完整过程,包括 dns 解析、请求数据发送、服务器处理、请求数据读取的整个流程。
  2. 组装过滤器链,开始执行请求流程。
  3. 回调请求结果,通知 Dispatcher 请求已经执行完。
    override fun run() {
          threadName("OkHttp ${redactedUrl()}") {
            var signalledCallback = false
            //使用AsyncTimeout监听请求是否超时,会开启一个子线程,线程名称 Okio Watchdog
            //超时后会调用Call的cancel取消请求
            timeout.enter()
            try {
                //组装过滤器链 开始执行请求流程
              val response = getResponseWithInterceptorChain()
              signalledCallback = true
                //回调请求结果
              responseCallback.onResponse(this@RealCall, response)
            } catch (e: IOException) {
              if (signalledCallback) {
                // Do not signal the callback twice!
                Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e)
              } else {
                responseCallback.onFailure(this@RealCall, e)
              }
            } catch (t: Throwable) {
              cancel()
              if (!signalledCallback) {
                val canceledException = IOException("canceled due to $t")
                canceledException.addSuppressed(t)
                responseCallback.onFailure(this@RealCall, canceledException)
              }
              throw t
            } finally {
                //请求执行完成
              client.dispatcher.finished(this)
            }
          }
        }
      }
Dispatcher
Dispatcher 作用

Dispatcher 是用来管理连接和分发请求的,使用线程池执行异步任务。默认使用的线程池是缓存线程池,可以在构建 OkHttpClient 的时候通过 Dispatcher 的构造参数传入自己的线程池。

Dispatcher 缓存线程池

缓存线程池使用同步队列,核心线程数为 0。

提交任务的时候,如果当前没有线程在取任务就会开启新线程执行,也就是说如果当前线程都在忙于执行请求,会立刻开启一个新线程。缓存线程池吞吐量高,响应速度快,但是并发高的情况下会创建大量线程,占用系统资源。

Dispatcher 缓存线程池用途

移动客户端的网络请求特点是并发量少,大多数情况只有 2、3 个同时发出的请求,但是由于大多请求都是由用户触发的请求,因此对响应速度要求较高。缓存线程池恰好满足了移动端的网络需求特点。 为了避免请求过多大量创建线程,因此使用两个队列限制异步请求的数量。同时执行的最大请求数量是 64,如果使用缓存线程池,也就相当于限制了同时运行的最大线程数量是 64。相同域名的最大请求数量是 5。

Dispatcher 队列定义

readyAsyncCalls

调用 enqueue 方法提交给 Dispatcher 的请求,如果没有提交给线程池执行,那么提交给线程池会从队列中移除。

runningAsyncCalls

正在线程池中执行的异步请求,还没有执行完,执行完会从队列中移除。

runningSyncCalls

正在执行的同步请求,还没有执行完,执行完会从该队列中移除。

Dispatcher 成员变量
    //同时执行的最大请求的数量
    var maxRequests = 64
    //每个域名可以同时执行的最大请求数量
    var maxRequestsPerHost = 5


      private var executorServiceOrNull: ExecutorService? = null
    //线程池默认使用缓存线程池
      @get:Synchronized
      @get:JvmName("executorService") val executorService: ExecutorService
        get() 
{
          if (executorServiceOrNull == null) {
            executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
                SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
          }
          return executorServiceOrNull!!
        }

     /** 调用enqueue提交给Dispatcher的请求,还没有提交到线程池执行 */
      private val readyAsyncCalls = ArrayDeque<AsyncCall>()

      /** 提交到线程池正在执行的异步请求,还没有执行完 */
      private val runningAsyncCalls = ArrayDeque<AsyncCall>()

      /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
      private val runningSyncCalls = ArrayDeque<RealCall>()

      constructor(executorService: ExecutorService) : this() {
        this.executorServiceOrNull = executorService
      }
Dispatcher 异步请求 enqueue
  1. 把 AsyncCall 添加到待执行队列 。
  2. 非 webSocket 请求,从待执行异步队列和已执行异步队列中查找相同 host 的请求,把已经存在的 call 的 callsPerHost 拷贝到新 call 的 callsPerHost。这样做的目的是,例如第一个请求查找不到相同 host 的请求,因此 callsPerHost 是 0,添加到线程池中,callsPerHost 变成了 1。第 2 个请求从以执行队列中查找到了相同 host 的请求,拷贝 callsPerHost,新 AsyncCall 的 callsPerHost 就变成了 1。这样传递下去,新添加的 AsyncCall 中的 callsPerHost 就是该 host 同时执行的请求数量。
  3. 提交给线程池执行
    internal fun enqueue(call: AsyncCall) {
      synchronized(this) {
        //1.添加到待执行队列
        readyAsyncCalls.add(call)

        // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
        // the same host.
        if (!call.call.forWebSocket) {
            //2.不是webSocket请求 从待执行异步队列和已执行异步队列中查找相同host的请求
            //把已经存在的call的callsPerHost拷贝到新call的callsPerHost
          val existingCall = findExistingCallWithHost(call.host)
          if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
        }
      }
        //3.提交给线程池执行
      promoteAndExecute()
    }
promoteAndExecute 执行流程
private fun promoteAndExecute(): Boolean {
  this.assertThreadDoesntHoldLock()

  val executableCalls = mutableListOf<AsyncCall>()
  val isRunning: Boolean
  synchronized(this) 
{
      //1.遍历带执行队列
    val i = readyAsyncCalls.iterator()
    while (i.hasNext()) {
      val asyncCall = i.next()
      //正在执行的请求数量大于64 直接退出循环
      if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
      //单个域名的最大请求数量大于5 处理下一个请求
      if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.
      //将请求从待执行队列移除 添加到已执行队列中
      i.remove()
      //增加相同域名的请求数量
      asyncCall.callsPerHost.incrementAndGet()
      //添加到 临时的可执行队列
      executableCalls.add(asyncCall)
      runningAsyncCalls.add(asyncCall)
    }

    isRunning = runningCallsCount() > 0
  }
  //遍历可执行队列 提交给线程池执行
  for (i in 0 until executableCalls.size) {
    val asyncCall = executableCalls[i]
    asyncCall.executeOn(executorService)
  }
  //isRunning是同步已执行请求和异步已执行请求的任务总量 isRunning>0说明有任务在执行 =0说明没有任务在执行
  return isRunning
}

遍历待执行队列,如果已执行队列的任务数量大于 64,跳出循环,如果当前 asyncCall 的相同域名请求数量大于 5,处理下一个请求。

将请求从待执行队列移除,添加到已执行队列和临时可执行队列中,更新 asyncCall 中的 callsPerHost。

遍历临时可执行队列,把任务添加到线程池中执行。

返回是否有任务在执行

ConnectionPool

ConnectionPool 使用代理模式,被代理类是 RealConnectionPool,在此基础上提供了一些开发功能。默认的最大限制连接数是 5,保持连接的最大时长是 5 分钟。

constructor() : this(5, 5, TimeUnit.MINUTES)

/** Returns the number of idle connections in the pool. */
fun idleConnectionCount(): Int = delegate.idleConnectionCount()

/** Returns total number of connections in the pool. */
fun connectionCount(): Int = delegate.connectionCount()

/** Close and remove all idle connections in the pool. */
//清除和关闭连接池中的所有连接
fun evictAll() {
  delegate.evictAll()
}

//RealConnectionPool
//连接存活的最长时间 默认是5分钟
private val keepAliveDurationNs: Long = timeUnit.toNanos(keepAliveDuration)

private val cleanupQueue: TaskQueue = taskRunner.newQueue()
//清理连接的任务 TaskRunner中使用一个缓存线程池执行改任务
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
  override fun runOnce() = cleanup(System.nanoTime())
}

/**
 *  使用cas无锁队列存储RealConnection
 */
private val connections = ConcurrentLinkedQueue<RealConnection>()

3.4.2 ThreadLocal

ThreadLocal 原理

ThreadLocal 通过线程数据隔离的方式来解决并发数据访问问题,每个线程都有自己的数据副本,ThreadLocal 的原理图如下

alt

线程数据隔离的核心是每个 Thread 对象都有一个属于自己的 ThreadLocalMap 对象,ThreadLocalMap 通过数组实现数据存取,每个数组元素都是一个 Entry。

Entry 用 ThreadLocal 做为 key,value 是我们要存放的数据。

ThreadLocal

一个 ThreadLocal 只能存取一种类型的数据,存取多种类型的数据可以使用多个 ThreadLocal,也可以把数据封装到同一个对象中。


public class ThreadLocal<T{

    private final int threadLocalHashCode = nextHashCode();

    //生成ThreadLocal的hashcode的原子计数器
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
       两个ThreadLocal的hash值的间隔差
     */

    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
       hash值 以HASH_INCREMENT累加
     */

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

ThreadLocal 的 hash 值是通过计数器自增生成的,使用多个 ThreadLocal 的情况下会出现 hash 冲突。

ThreadLocalMap

ThreadLocalMap 使用 WeakReference,为监听 ThreadLocal 是否被回收。

ThreadLocal.ThreadLocalMap threadLocals = null;
//当创建子线程的时候,子线程可以得到父线程的inheritableThreadLocals
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//-------------------------------------------------------------

当 Entry.get()返回 null 的时候,说明 ThreadLocal 已经被回收,这时就要将 Entry 中的 value 引用设置为 null,避免出现内存泄漏。

//使用WeakReference监听ThreadLocal是否被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;

      Entry(ThreadLocal<?> k,Object v) {
          super(k);
          value = v;
      }
  }
   //初始化容量 16
   private static final int INITIAL_CAPACITY = 16;

  /**
   * The table,resized as necessary.
   * table.length MUST always be a power of two.
   Entry数组
   */

  private Entry[] table;

  /**
   * The number of entries in the table.
     实际存储的数据量
   */

  private int size = 0;

  /**
   * The next size value at which to resize.
     扩容阈值 默认是size达到容量的2/3时扩容
   */

  private int threshold; // Default to 0

为什么使用 WeakReference,为监听 ThreadLocal 是否被回收。当 Entry.get()返回 null 的时候,说明 ThreadLocal 已经被回收,这时就要将 Entry 中的 value 引用设置为 null,避免出现内存泄漏。

ThreadLocal 核心方法
set
  1. 获取当前线程 Thread 引用,获取或创建 Thread 中的 ThreadLocalMap,调用 ThreadLocalMap 的 set 方法。
  2. 让 ThreadLocal 的 hash 值和数组长度做与运算得到对应的数组索引 index。
  3. 线性探测法解决 hash 冲突,如果数组索引位置的 Entry 是空的,创建一个新的 Entry 设置到该位置。如果数组索引位置的 key 和当前 ThreadLocal 地址相同,用新值更新旧值。否则就是出现了 hash 冲突,从当前的 i 开始向后查找,直到找到一个空的位置为止。在查找的过程中,如果发现 ThreadLocal 已经被回收,就会调用 replaceStaleEntry 方法清理对应位置的 value 数据。
  4. 创建 Entry,增加 size。
  5. 先清理 ThreadLocal 已经被回收的 Entry,然后判断是否需要扩容。
public void set(T value) {
        //获取当前线程引用
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap 如果是空的会创建一个
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //设置值
            map.set(this,value);
        else
            createMap(t,value);
    }

    private void set(ThreadLocal<?> key,Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones,in which case,a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            //1.对hashcode做与运算 确定ThreadLocal在数组中的索引
            int i = key.threadLocalHashCode & (len-1);
            //2.从当前索引开始向后查找,找到key直接赋值返回,否则找到一个Entry为空的位置,记录i
            //用线性探测的方式解决hash冲突的问题
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i,len)]) {
                ThreadLocal<?> k = e.get();
                //key已经存在 直接替换旧值
                if (k == key) {
                    e.value = value;
                    return;
                }
                //key为空 清理对应的value数据
                if (k == null) {
                    replaceStaleEntry(key,value,i);
                    return;
                }
            }
            //3.创建一个新的Entry
            tab[i] = new Entry(key,value);
            int sz = ++size;
            //4.先清理key为空的Entry 然后判断是否需要扩容
            if (!cleanSomeSlots(i,sz) && sz >= threshold)
                rehash();
        }
get
  1. 获取当前线程的 ThreadLocalMap,调用 ThreadLocalMap 的 getEntry 获取 Entry。
  2. 根据 hashcode 和数组长度计算对应的数组索引,如果对应位置的 Entry 不为空并且 key 和当前 ThreadLocal 相同,返回 Entry。否则执行第 3 步。
  3. set 数据的时候可能出现了 hash 冲突,从数组为 i 的位置开始向后查找,如果找到了对应的 key 就返回。如果遇到一个位置 Entry 为 null,说明后续的位置都是 null,因此直接返回 null。如果遇到 ThreadLocal 被回收的情况,调用 expungeStaleEntry 移除过期的数据。
  4. hash 冲突情况下,Entry 的查找采用线性查找。因此,在同一个 Thread 中使用大量 ThreadLocal 的情况下会比较消耗性能。
public T get() {
    //1.获取当前线程的ThreadLocalMap
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //获取当前ThreadLocal对应的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
//---------------------------------------------
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //2.Entry不为空并且key和当前ThreadLocal相同
    if (e != null && e.get() == key)
        return e;
    else
        //3.可能出现了hash冲突 从i开始向数组后面查找
        return getEntryAfterMiss(key,i,e);
}
//---------------------------------------------
private Entry getEntryAfterMiss(ThreadLocal<?> key,int i,Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    //从i开始循环遍历 当Entry=null的时候 说明后面的肯定都是空的
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i,len);
        e = tab[i];
    }
    return null;
}
扩容
  1. 当数据量达到数组容量的 2/3 的时候才会扩容,扩容后容量是之前容量的 2 倍,扩容后会把旧数组中的数据拷贝到新数组中,通过 hash 运算和线性探测计算元素在新数组中的索引。
  2. 扩容过程是不存在线程安全问题的,因为每个线程都有自己的 ThreadLocalMap。
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    //扩容速度为原先容量的2倍
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    //把原先的数据拷贝到新数组中
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null// Help the GC
            } else {
                //重新计算hash值 解决hash冲突
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h,newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    //重新设置新的扩容阈值 为数组长度的2/3
    setThreshold(newLen);
    size = count;
    table = newTab;
}
内存泄露

alt

当 ThreadLocal 的强引用被置为 null 的时候,可能会被 gc 回收,此时就无法通过 ThreadLocal 访问到它存储的资源了。

但是还存在一条引用链路,从 Thread-->ThreadLocalMap-->Entry-->value 的引用链,因此在线程没有结束的情况下,就会发送内存泄漏。如果使用的是线程池,内存泄漏的持续时间就会比较长。 ThreadLocal 已经解决了问题,通过弱引用来监听 ThreadLocal 是否被回收,被回收的时候,断开 value 和 Entry 之间的引用链。

使用场景

多线程下载文件或从设备导入图片的时候,统计每个线程处理的文件数量,失败的数量、成功的数量等。

3.4.3 alpha

对于大型 App 来说,启动任务多,任务依赖复杂。保障任务逻辑的单一性,解耦启动任务逻辑,合理利用多核 CPU 优势,提高线程运行效率是重点关注的问题。

为了利用多核 cpu,提高任务执行效率,让单个任务职责更加清晰,代码更加优雅进而提高启动速度,我们会尽可能让这些工作并发进行。

但这些工作之间可能存在前后依赖的关系,我们又需要想办法保证他们执行顺序的正确性。

所以我们要做的工作是将任务颗粒化,定义好自己的任务,并描述它依赖的任务,将它添加到 Project 中。框架会自动并发有序地执行这些任务,并将执行的结果抛出来。

那么怎样对任务进行分类呢?

任务进行分类策阅可以通过 Alpha 等启动器把启动任务管理起来。具体分为四个步骤: 第一个步骤是将启动任务原子化,分为各个任务。

第二个步骤是使用有向无环图管理启动任务。前后依赖的任务串行,无依赖的任务线程池化并行。优先级高的任务在前,优先级低的任务在后。

第三个步骤是启动任务集中化,分任务区块:核心任务,主要任务,延迟任务,懒加载任务。核心任务在 attachBaseContext 中执行,主要任务在启动页或首页执行,延迟任务在首页后空闲时间执行,懒加载任务在特定的时机执行。

最后一个步骤是启动任务统计化,提供任务的耗时统计和卡口。

任务管理框架图参考如下:

alt

而阿里巴巴的 alpha 启动器恰好解决了启动任务管理不合理的业务痛点, 那么启动任务管理不合理的业务痛点具体表现有哪些呢?

业务痛点

启动任务管理不合理的业务痛点具体表现有如下五个特征:

  1. 多线程管理
  2. 任务的优先级
  3. 任务之间的先后关系
  4. 任务是否需要在主线程执行
  5. 多进程处理
源码分析

为了深入学习阿里巴巴的 alpha 启动器的原理, 小木箱通过以下流程图带大家认识一下阿里巴巴的 alpha 启动器的核心知识。

alt

AlphaManager.getInstance(mContext).start()

start 判断是否为当前进程和是否能匹配到相关进程任务, 具体参数配置如下:

  • MAIN_PROCESS_MODE : 主进程任务
  • SECONDARY_PROCESS_MODE :非主进程任务
  • ALL_PROCESS_MODE:适用于所有进程的任务
    public void start() {
        Project project = null;

        do {
            //1.是否有为当前进程单独配置的Project,此为最高优先级
            if (mProjectForCurrentProcess != null) {
                project = (Project) mProjectForCurrentProcess;
                break;
            }

            //2.如果当前是主进程,是否有配置主进程Project
            if (AlphaUtils.isInMainProcess(mContext)
                    && mProjectArray.indexOfKey(MAIN_PROCESS_MODE) >= 0) {
                project = (Project) mProjectArray.get(MAIN_PROCESS_MODE);
                break;
            }

            //3.如果是非主进程,是否有配置非主进程的Project
            if (!AlphaUtils.isInMainProcess(mContext)
                    && mProjectArray.indexOfKey(SECONDARY_PROCESS_MODE) >= 0) {
                project = (Project) mProjectArray.get(SECONDARY_PROCESS_MODE);
                break;
            }

            //4.是否有配置适用所有进程的Project
            if (mProjectArray.indexOfKey(ALL_PROCESS_MODE) >= 0) {
                project = (Project) mProjectArray.get(ALL_PROCESS_MODE);
                break;
            }
        } while (false);

        if (project != null) {
            addListeners(project);
            project.start();
        } else {
            AlphaLog.e(AlphaLog.GLOBAL_TAG, "No startup project for current process.");
        }
    }

Where: 配置相关进程任务位置?

    public void addProject(Task project, int mode) {
        if (project == null) {
            throw new IllegalArgumentException("project is null");
        }

        if (mode < MAIN_PROCESS_MODE || mode > ALL_PROCESS_MODE) {
            throw new IllegalArgumentException("No such mode: " + mode);
        }

        if (AlphaUtils.isMatchMode(mContext, mode)) {
            mProjectArray.put(mode, project);
        }
    }
project start
    @Override
    public void start() {
        mStartTask.start();
    }

开启一个mStartTask?这个mStartTask是之前设置的些任务中第一个任务吗?

        //Project.java
        private void init() {
        ...
            mProject = new Project();
            mFinishTask = new AnchorTask(false, "==AlphaDefaultFinishTask==");
            mFinishTask.setProjectLifecycleCallbacks(mProject);
            mStartTask = new AnchorTask(true, "==AlphaDefaultStartTask==");
            mStartTask.setProjectLifecycleCallbacks(mProject);
            mProject.setStartTask(mStartTask);
            mProject.setFinishTask(mFinishTask);
       ...
        }


    private static class AnchorTask extends Task {
        private boolean mIsStartTask = true;
        private OnProjectExecuteListener mExecuteListener;

        public AnchorTask(boolean isStartTask, String name) {
            super(name);
            mIsStartTask = isStartTask;
        }

        public void setProjectLifecycleCallbacks(OnProjectExecuteListener callbacks) {
            mExecuteListener = callbacks;
        }

        @Override
        public void run() {
            if (mExecuteListener != null) {

                if (mIsStartTask) {
                    mExecuteListener.onProjectStart();
                } else {
                    mExecuteListener.onProjectFinish();
                }
            }
        }

    }

Why: 定义一个开始任务和一个结束任务

执行角度: 一个任务序列必须有一个开始节点和一个结束节点。

生产角度: 多个任务可以同时开始,而且有多个任务可以同时作为结束点

设计原则:

  1. 设置两个节点 方便控制整个流程
  2. 标记流程开始和结束,方便 任务的监听
AnchorTask 父类 Task
  1. 定义 Runnable,判断是否主线程,并执行这个 Runnable,穿插了一些状态的改变
  2. Runnable内部主要是执行了 Task.this.run(),并执行了任务本身。
  3. 其中 setThreadPriority方法设置了线程优先级,比如 THREAD_PRIORITY_DEFAULT线程优先级处理 CPU 资源竞争问题,不影响 Task 之间的优先级。
  4. 如果在主线程执行任务,通过 Handler(sHandler)将事件传递给主线程执行。
  5. 如果在非主线程执行的任务,通过 线程池(sExecutor)执行线程任务。
    public synchronized void start() {
        ...
        switchState(STATE_WAIT);

        if (mInternalRunnable == null) {
            mInternalRunnable = new Runnable() {
                @Override
                public void run() {
                    android.os.Process.setThreadPriority(mThreadPriority);
                    long startTime = System.currentTimeMillis();

                    switchState(STATE_RUNNING);
                    Task.this.run();
                    switchState(STATE_FINISHED);

                    long finishTime = System.currentTimeMillis();
                    recordTime((finishTime - startTime));

                    notifyFinished();
                    recycle();
                }
            };
        }

        if (mIsInUiThread) {
            sHandler.post(mInternalRunnable);
        } else {
            sExecutor.execute(mInternalRunnable);
        }
    }
notifyFinished

mSuccessorList 排序

「紧后任务列表」,也就是接下来要执行的任务列表。所以流程就是先把当前任务之后的任务列表进行一个排序,根据优先级排序。然后按顺序执行onPredecessorFinished方法。

如果紧后任务列表为空,也就代表没有后续任务了,那么就会走onTaskFinish回调方法,告知当前 Project 已经执行完毕。

遍历mSuccessorList列表,执行onPredecessorFinished方法

监听回调onTaskFinish方法

    void notifyFinished() {
        if (!mSuccessorList.isEmpty()) {
            AlphaUtils.sort(mSuccessorList);

            for (Task task : mSuccessorList) {
                task.onPredecessorFinished(this);
            }
        }

        if (!mTaskFinishListeners.isEmpty()) {
            for (OnTaskFinishListener listener : mTaskFinishListeners) {
                listener.onTaskFinish(mName);
            }

            mTaskFinishListeners.clear();
        }
    }

How: 紧后任务怎么加进来?紧后任务怎么排序?

onPredecessorFinished

紧后任务列表是通过after方法实现的

builder.add(Task2).after(Task1),所以 after 代表 Task2 要在 Task1 后面执行,Task2 成了 Task1 的紧后任务。

同理,Task1 也就成了 Task2 的紧前任务。是通过addPredecessor方法,在添加紧后任务同时也添加紧前任务。

紧前任务添加了有什么用呢?难不成还倒退回去执行?

如果有多个任务的紧后任务都是一个,比如这种情况:builder.add(Task4).after(Task2,Task3)

Task4 是 Task2 和 Task3 的紧后任务,所以在 Task2 执行完之后,还要判断 Task3 是否执行成功,然后才能执行 Task4,这就是紧前任务列表的作用。

onPredecessorFinished就是做这样的工作的。

How: 紧后任务列表的排序是如何排序呢?

通过getExecutePriority方法获取 task 执行优先级数字,正序排列,越小任务执行时机越早。

setExecutePriority方法设置了排序优先级。

  • 各种回调:包括一些 task 的回调,project 的回调。
  • 日志记录:比如耗时时间的记录,刚才执行任务时候的 recordTime方法,就是记录了每个 task 的耗时。
  • Task 脚本化:通过 XmlPullParser类来解析 xml 数据,然后生成 Project 来配置 Project 的 Task。
  • 设计模式:构建 Project 的建造者模式,通过传入 task 名称可以创建 Task 工厂模式。
    //1、紧后任务添加
    public Builder after(Task task) {
        task.addSuccessor(mCacheTask);
        mFinishTask.removePredecessor(task);
        mIsSetPosition = true;
        return Builder.this;
    }

    void addSuccessor(Task task) {
        task.addPredecessor(this);
        mSuccessorList.add(task);
    }

    //2、紧后任务列表排序
    public static void sort(List<Task> tasks) {
        if (tasks.size() <= 1) {
            return;
        }
        Collections.sort(tasks, sTaskComparator);
    }

    private static Comparator<Task> sTaskComparator = new Comparator<Task>() {
        @Override
        public int compare(Task lhs, Task rhs) {
            return lhs.getExecutePriority() - rhs.getExecutePriority();
        }
    };

    //3、紧后任务执行
    synchronized void onPredecessorFinished(Task beforeTask) {

        if (mPredecessorSet.isEmpty()) {
            return;
        }

        mPredecessorSet.remove(beforeTask);
        if (mPredecessorSet.isEmpty()) {
            start();
        }

    }

3.4.4 一键暂停和恢复下载

百度网盘批量下载文件,如何实现一键暂停和恢复?可以扩展线程池实现一个可以暂停和恢复的线程池

线程池可以让所有 Worker 暂停新任务的执行,但是正在下载的任务并没有被暂停,所以需要在下载任务中处理暂停和恢复的情况。

当任务暂停的时候,退出读取数据的循环,关闭连接。

因为暂停的时间肯能比较长,为了防止资源占用时间较长,需要先关闭连接,循环退出后有两种处理方式。

循环退出即任务执行完,等回复执行的时候重新提交该任务进行断点续传,由于线程池的排队机制,暂停的任务将无法继续执行,而是在队列中排队,不符合需求。

循环退出后,在外层循环判断当前任务是否下载完,如果没有继续断点续传,续传前判断执行是否暂停,如果暂停则等待,当恢复执行的时候,唤醒当前线程.。

在 beforeExecute 方法中将任务添加到集合中,在 afterExecute 中将任务移除。因此,当暂停线程池的时候,集合中的任务就是正在执行的任务,依次遍历调用任务的 pause 方法,当恢复线程池的时候,依次遍历调用任务的 resume 方法。

public class CustomExecutor extends ThreadPoolExecutor {
    private List<DownLoadTask>  executingTask=new LinkedList<>();
    private volatile  boolean isRunning;
    private ReentrantLock pauseLock=new ReentrantLock();
    private Condition pauseCondition=pauseLock.newCondition();
    public CustomExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue) {
        super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);
    }

    public void pause(){
        if (!isRunning){
            return;
        }
        synchronized (this){
            Iterator<DownLoadTask> iterator = executingTask.iterator();
            while (iterator.hasNext()){
                DownLoadTask task = iterator.next();
                task.pause();
            }
            isRunning=false;
        }

    }
    public void resume(){
        if (isRunning){
            return;
        }
        pauseLock.lock();
        try {
            pauseCondition.signalAll();
            Iterator<DownLoadTask> iterator = executingTask.iterator();
            while (iterator.hasNext()){
                DownLoadTask task = iterator.next();
                task.resume();
            }
            isRunning=true;
        } finally {
            pauseLock.unlock();
        }
    }
    @Override
    protected void beforeExecute(Thread t,Runnable r) {
        super.beforeExecute(t,r);
        pauseLock.lock();
        try {
            while (!isRunning){
                pauseCondition.await();
            }
        } catch (InterruptedException e) {
            t.interrupt();
        } finally {
            pauseLock.unlock();
        }

        synchronized (this){
            if (r instanceof DownLoadTask){
                executingTask.add((DownLoadTask) r);
            }
        }
    }
    @Override
    protected void afterExecute(Runnable r,Throwable t) {
        super.afterExecute(r,t);
        synchronized (this){
            if (r instanceof DownLoadTask){
                executingTask.remove(r);
            }
        }
    }
}

3.5 线程池性能优化

  1. 选择合适的线程池大小:线程池大小应该根据系统的处理能力、资源限制以及任务的特性来选择。过小的线程池会导致任务排队等待,过大的线程池会导致资源浪费和调度开销增加。一般来说,线程池大小应该设置为处理器核心数的两倍。
  2. 使用合适的队列类型:线程池的任务队列可以是阻塞队列或非阻塞队列。阻塞队列可以避免任务排队等待时的 busy waiting,但会增加系统开销。非阻塞队列可以减少系统开销,但会增加任务排队等待的时间。选择适合任务特性和处理需求的队列类型可以提高线程池性能。
  3. 避免任务过多和过少:过多的任务会导致线程池过载,过少的任务会导致线程池资源浪费。应该根据实际任务需求和系统处理能力来合理分配任务,避免过多或过少的任务。
  4. 合理设置线程池参数:线程池的参数包括核心线程数、最大线程数、线程存活时间和任务队列大小等。应该根据系统特性和任务需求来设置这些参数,以提高线程池性能。
  5. 优化任务执行效率:线程池的性能也与任务执行效率有关。可以优化任务代码,避免耗时操作和竞争条件,提高任务执行效率,从而提高线程池性能。
  6. 监控线程池状态:应该定期监控线程池的状态,包括线程池大小、线程使用情况、任务队列情况等。及时发现问题并进行调整,以保证线程池的性能。

3.6 线程池注意事项

alt
线程池监控

项目中出现过记录丢失的情况,有一个记录保存过程中卡住,从而导致后面的任务都存储到了阻塞队列 BlockingQueue 中。

设备重启后,直接导致队列中的记录全部丢失。

解决方法是对线程池的运行状态进行监控,正常情况下阻塞队列 BlockingQueue 里应该是没有任务的,当阻塞队列 BlockingQueue 中的任务数量超过某个阈值后触发异常任务管理机制。

不同任务类型应该使用不同的线程池,不要把所有任务都用同一个线程池执行。主要从任务类型业务场景任务时间三个维度去考量。

任务类型

任务主要分为CPU 密集IO 密集两种类型,现代计算机 IO 都是通过 DMA 直接存储访问器处理的,处理完成后在给 CPU 发一个中断,CPU 在继续执行。

如果把这两种任务放到一个线程,读取 IO 文件的时候,线程就需要等待 IO 完成才能继续执行。

文件读取和数据处理不能并发处理,任务执行时间就增长了。

业务场景

app 中比较常见的有前台任务和后台任务,面向用户操作的是前台任务,前台任务对响应速度要求比较高,例如用户点击按钮请求服务器。

后台任务是长时间在后台运行的任务,例如百度网盘批量下载文件。

如果前台任务用 Okhttp 异步请求,后台任务也使用 Okhttp 异步请求,相当于都使用 Okhttp 缓存线程池,可能会导致线程数量大量增加。

这种情况后台下载文件应该自定义线程池,使用 Okhttp 同步请求。

任务时间

时间长的任务和时间短的任务不要使用同一个线程池,会导致时间短的任务不能及时执行。

3.7 线程池线程数量确定

CPU 密集型

线程数可以设置为 N+1,N 是 CPU 核心数。多出来一个是为了防止线程缺页中断或其他原因导致的任务暂停,这时候多出来的线程就可以充分使用 CPU 的空闲时间。

IO 密集型

线程数可以设置为 N*2,IO 密集型任务不占用 CPU,现代计算机都是通过 DMA 直接内存访问控制器处理的。在执行这类任务的时候,CPU 会有许多空闲时间执行其他任务,因此可以多设置一些线程。

通用公式

IO 耗时占比越多,线程数量越多。线程数通用公式参考如下: ```线程数 = CPU 核心数 * (1+ IO 耗时/CPU 耗时)`

3.8 线程池业务防劣化 Lint 工具

为什么不建议使用 Executors 创建线程? 实际开发中,不建议使用 Executors 创建线程池,有如下三个原因:

  1. 单线程和固定数量线程线程池的阻塞队列 BlockingQueue 都没有设置大小,如果有一个任务阻塞,可能会导致队列中的任务无限增加,最终触发 oom 或者导致任务全部丢失。

  2. 缓存线程池线程数量无上限,如果任务过多,并且任务执行时间都很长,可能会导致线程数量无限增长,最终触发 oom。

  3. 不能指定任务拒绝策略,默认的拒绝策略为 AbortPolicy,如果不设置可能会导致程序崩溃。

那么该如何在编译期去发现上述问题呢? Android Lint 是 Google 提供给 Android 开发者的静态代码检查工具。

使用 Lint 对 Android 工程代码进行扫描和检查,可以发现代码潜在的问题,提醒程序员及早修正。

通过 lint 工具防劣化, 提醒业务使用架构组独享的线程池。

下面就由小木箱带大家实现一下禁止使用 Executors 创建线程池的 Lint 工具吧~

public class ThreadPoolDetector extends Detector implements Detector.JavaScanner {

    public static final Issue ISSUE = Issue.create(
            "创建线程池"
            "避免自己创建ThreadPool"
            "请勿直接使用Executors创建线程,建议使用统一的线程池管理工具类"
            Category.PERFORMANCE,
            6
            Severity.WARNING,
            new Implementation(ThreadPoolDetector.class, Scope.JAVA_FILE_SCOPE)
    )
;

    @Override
    public List<Class<? extends Node>> getApplicableNodeTypes() {
        return ImmutableList.of(MethodCall.class);
    }

    @Override
    public void visitMethodCall(@NonNull JavaContext context, @NonNull UCall node, @NonNull PsiMethodCallExpression call) {
        if (node.getMethodName().equals("newFixedThreadPool") || node.getMethodName().equals("newCachedThreadPool") || node.getMethodName().equals("newSingleThreadExecutor")) {
            context.report(ISSUE, node, context.getLocation(node), "不建议使用Executors创建线程, 改用ThreadPoolExecutor");
        }
    }
}

四、SCQA 分析线程池

答案未来将上传 B 站, 请关注 B 站号: 小木箱成长营

  1. 日常工作中有用到线程池吗?什么是线程池?为什么要使用线程池?
  2. 工作线程 Worker 继承 AQS 实现了锁机制,那 ThreadPoolExecutor 都用到了哪些锁?为什么要用锁?
  3. 项目中是怎样使用线程池的?Executors 了解吗?
  4. 线程池有哪些参数?
  5. 线程池的运行原理是什么?
  6. 线程池的执行流程?
  7. 如何合理配置线程池?
  8. 核心线程能否退出?
  9. 拒绝策略有哪些?适用场景是怎么样的?
  10. 使用线程池的过程中遇到过哪些坑或者需要注意的地方?
  11. 如何监控线程池?
  12. JDK 自带的线程池种类有哪些?
  13. 为什么不推荐使用 JDK 自带的线程池?
  14. 如何合理设置核心线程数的大小?
  15. 说说 submit 和 execute 两个方法有什么区别?
  16. shutdownNow() 和 shutdown() 两个方法有什么区别?
  17. 调用了 shutdownNow 或者 shutdown,线程一定会退出么?
  18. 什么是阻塞队列?阻塞队列有哪些?为什么线程池要使用阻塞队列?
  19. 通过 ThreadPoolExecutor 来创建线程池,那核心参数设置多少合适呢?

五、结语

三大分析法分析线程池主要分为四部分,第一部分是 4W2H 分析线程池,第二部分是 MECE 分析线程池,第三部分是 SCQA 分析线程池,最后一部分是结语。

其中,4W2H 分析线程池主要围绕线程池提出了 6 个高价值问题。

其中,MECE 分析线程池主要分为线程池基本操作、线程池生命周期、线程池工作原理、线程池代码案例分析、线程池的性能优化、线程池注意事项、线程池线程数量确定和线程池业务防劣化 8 部分。

线程池学习的重要性是不可忽视的。在现代互联网时代,线程池是一种重要的多线程编程技术,能够提高程序的性能、稳定性和可靠性。因此,学习线程池成为了每一位 Android 开发工程师的必备技能。

希望通过通过本文线程池学习,能够让您更快的通过职场面试同时也能解决工作中的业务痛点。

如果你觉的小木箱的文章对你有所帮助,那么可以关注公众号小木箱成长营。让你的知识和视野得到更广阔的拓展吧,下一篇将介绍 Java 并发关键字那些事,同样是并发编程核心内容。 今天就到这里啦,我是小木箱,我们下一篇见~

参考资料:

更快!更高效!异步启动框架 Alpha 完全解析 更快!更高效!异步启动框架 Alpha 完全解析

本文由 mdnice 多平台发布

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值