【Java并发编程】线程池ThreadPoolExecutor实战及其原理分析

4 Executor线程池

4.1 概述

线程发生异常,会被移除线程池,但是如果是核心线程,会创建一个新核心线程;

4.1.1 线程池的好处

  • 降低资源消耗

降低了频繁创建线程和销毁线程开销,线程可重复利用;

  • 提高响应速度
  • 提高线程可管理性

统一对线程分配、调优、监控,线程是系统的稀缺资源;

4.1.2 线程池的执行流程

(1)execute()和submit()的区别

public void execute(Runnable command) // 
public Future<?> submit(Runnable task) // 继承自AbstractExecutorService

都是向线程池提交一个任务,交由线程池执行;
execute()方法是Executor中申明的方法,在ThreadPoolExecutor具体实现;
submit()方法是ExecutorService中申明的方法,在AbstractExecutorService具体实现,实际还是调用execute()方法,可以通过Furture返回执行结果:

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

注意:

  • 提交一个Runnable时,不管当前线程池中的线程是否空闲,只要数量小于核心线程数就会创建新线程。
  • ThreadPoolExecutor相当于是非公平的,比如队列满了之后提交的Runnable可能会比正在排队的Runnable先执行。

(2)execute()执行步骤

【Java并发编程】线程池ThreadPoolExecutor实战及其原理分析_第1张图片

代码分析:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
    	// 判断线程数是否小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
            // 小于,创建新线程,并添加当前任务到任务队列
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
    	// 线程数不小于核心线程数,offer()添加任务到任务队列
        if (isRunning(c) && workQueue.offer(command)) {
            // 添加任务队列成功
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 任务队列满了,添加任务队列失败,继续addWorker()判断线程池最大线程数
        else if (!addWorker(command, false))
            // 拒绝任务
            reject(command);
    }
private boolean addWorker(Runnable firstTask, boolean core) // 该方法为添加任务到线程池,其中有判断corePoolSize和maximumPoolSize逻辑

4.1.3 线程池的状态

  • RUNNING(运行中)

正常状态,会接收新任务,会处理等待队列任务;

  • SHUTDOWN(关闭)

不会接收新任务,会处理等待队列任务;

  • STOP(停止)

不会接收新任务,不会处理等待队列任务,中断正在执行的任务;

  • TIDYING(整齐)

所有线程都销毁了,workCount为0,线程池状态在转为TIDYING时会执行钩子方法terminated()方法;

  • TERMINATED(结束)

terminated()方法结束后的状态;

这五种状态并不能任意转换,只会有以下几种转换情况:
RUNNING -> SHUTDOWN:手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用shutdown()
(RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()触发,如果先调shutdown()紧着调shutdownNow(),就会发生SHUTDOWN -> STOP
SHUTDOWN -> TIDYING:队列为空并且线程池中没有线程时自动转换
STOP -> TIDYING:线程池中没有线程时自动转换(队列中可能还有任务)
TIDYING -> TERMINATED:terminated()执行完后就会自动转换

4.1.4 线程池为什么要用阻塞队列

线程池中的线程在运行过程中,执行完创建线程时绑定的第一个任务后,就会不断的从队列中获取任务并执行,那么如果队列中没有任务了,线程为了不自然消亡,就会阻塞获取队列任务,等着队列中有任务过来就会拿到任务从而去执行任务。
通过这种方法能最终确保,线程池中能保留指定个数的核心线程数,关键代码为:

try {
    Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
        workQueue.take();
    if (r != null)
        return r;
    timedOut = true;
} catch (InterruptedException retry) {
    timedOut = false;
}

某个线程在从队列获取任务时,会判断是否使用超时阻塞获取,我们可以认为非核心线程会poll(),核心线程会take(),非核心线程超过时间还没获取到任务后面就会自然消亡了。

4.1.5 线程池的核心线程数、最大线程数该如何设置

corePoolSize:核心线程数,表示线程池中的常驻线程的个数
maximumPoolSize:最大线程数,表示线程池中能开辟的最大线程个数

  • 根据服务是计算密集型还是IO密集型为基础进行设置,计算密集型应该尽量避免CPU线程上下文切换,IO密集型尽量避免线程一直阻塞等待;
  • 获取CPU核心数(可能是逻辑核心数):Runtime.getRuntime().availableProcessors()
  • CPU密集型任务:CPU核心数+1,这样既能充分利用CPU,也不至于有太多的上下文切换成本;
  • IO型任务:建议压测,或者先用公式计算出一个理论值(理论值通常都比较小)
  • 对于核心业务(访问频率高),可以把核心线程数设置为我们压测出来的结果,最大线程数可以等于核心线程数,或者大一点点,比如我们压测时可能会发现500个线程最佳,但是600个线程时也还行,此时600就可以为最大线程数
  • 对于非核心业务(访问频率不高),核心线程数可以比较小,避免操作系统去维护不必要的线程,最大线程数可以设置为我们计算或压测出来的结果。

你可能感兴趣的:(#,Java基础,java,线程池,Executor,并发编程)