《Java并发编程的艺术》读书笔记 - 第九章 - Java中的线程池

目录

前言

线程池的实现原理

线程池的 7 大参数

corePoolSize

maximumPoolSize

keepAliveTime

TimeUnit

BlockingQueue

ThreadFactory

RejectedExecutionHandler

线程池的运行

线程池的状态

线程池的使用

execute()

submit()

线程池的关闭

合理配置线程池

经验公式


前言

Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来 3 个好处。

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
  • 提高线程的可管理性:使用线程池可以对线程进行统一分配、调优和监控

线程池的实现原理

线程池的 7 大参数

corePoolSize

corePoolSize 是核心线程数。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务,也会创建线程。等到需要执行的任务数大于corePoolSize 时不再创建。

maximumPoolSize

线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列,这个参数就没什么效果。

keepAliveTime

超过核心线程数的线程在不执行任务时的存活时间。

TimeUnit

keepAliveTime 的时间单位。

BlockingQueue

用于保存等待执行的任务的阻塞队列。常见阻塞队列如下:

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO 排序元素。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作。
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

ThreadFactory

用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

RejectedExecutionHandler

当任务队列和线程池都满的时候采取的任务拒绝策略。

常见的拒绝策略:

  • AbortPolicy:直接抛出异常
  • CallerRunsPolicy:指定调用者所在线程来运行任务
  • DiscardOldestPolicy:丢弃队列中最近的一个任务,并执行当前任务
  • DiscardPolicy:不处理,直接丢弃

线程池的运行

当接收一个任务时,如果当前运行的线程数小于 corePoolSize,则创建新线程来执行任务(这一步骤需要获取全局锁)。如果运行的线程数等于或大于 corePoolSize,则将任务加入BlockingQueue。如果BlockingQueue 容量已满,则创建新的线程来处理任务(需要获取全局锁),若当前线程数大于 maxPoolSize,此时任务将会采用对应的拒绝策略。

图片来源:百度

举例:当前线程池参数如下,100 个任务同时执行

前 10 个任务创建核心线程数执行,11 - 60 个任务加入任务队列,61 - 80 个任务创建新线程执行,剩下 20 个任务直接抛弃。

线程池的状态

  • RUNNING:运行状态,线程池创建好之后就会进入此状态,如果不手动调用关闭方法,那么线程池在整个程序运行期间都是此状态。
  • SHUTDOWN:关闭状态,不再接受新任务提交,但是会将已保存在任务队列中的任务处理完。
  • STOP:停止状态,不再接受新任务提交,并且会中断当前正在执行的任务、放弃任务队列中已有的任务。
  • TIDYING:整理状态,所有的任务都执行完毕后(也包括任务队列中的任务执行完),当前线程池中的活动线程数降为 0 时的状态。到此状态之后,会调用线程池的 terminated() 方法。
  • TERMINATED:销毁状态,当执行完线程池的 terminated() 方法之后就会变为此状态。

图片来源:百度

线程池的使用

可以使用两个方法向线程池中提交任务,分别为 execute() 和 submit()方法。 

execute()

execute() 方法用于提交不需要返回值的任务 

    private static void execute() {
        poolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("execute...");
            }
        });
    }

submit()

submit() 方法用于提交需要返回值的任务 

    private static Object submit() throws ExecutionException, InterruptedException {
        Future<Object> submit = poolExecutor.submit(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                System.out.println("submit...");
                return "hello submit";
            }
        });

        return submit.get();
    }

线程池的关闭

可以通过线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow 首先将线程池的状态设置为 STOP 然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。

合理配置线程池

要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析:

  • 任务的性质:CPU 密集型任务、IO 密集型任务和混合任务。
  • 任务的优先级:高、中、低
  • 任务的执行时间:长、中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

    CPU 密集型任务应配置尽可能小的线程,如配置CPU数量 + 1 个线程的线程池。由于 IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 2 * CPU数量。可以通过 Runtime.getRuntime().availableProcessors()方法获得设备的CPU个数。

经验公式

线程数量 = CPU数量 * CPU期望利用率 * ( 1 + wait time / service time)
wait time / service time 被称为阻塞系数,CPU 密集型任务的阻塞系数为 0

  • wait time:等待IO完成时间
  • service time:CPU处理任务时间

    例如一个 8 核CPU,希望这部分工作的CPU使用率为 30%,任务等待 IO完成时间为 90ms,任务CPU处理时间为 10ms
    线程数量 = 8 * 50% * (1 + 90 / 10) = 24
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值