如何正确使用线程和线程池

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

前言

1 我们大部分人都搞不清楚线程池的执行流程,核心线程数和最大线程数的关系傻傻搞不清。

2 很多时候我们没有搞清楚我们讨论的到底是谁的线程。本文会重点标注出我们常说的 Java 线程和操作系统线程之间的区别和联系。

Java 线程

Java 线程分为六种状态,这里找到一张比较清晰的不同状态之间流转的图片。

线程状态流转图

只有在 Runnable 状态才是获得 CPU 真正执行的时候。

在图中也标识了不同的状态之间流转的条件,我们在调试的过程中,可能会使用 Thread.getState() 这个方法,用于打印当前线程状态。

这里就不细说每个具体的流转过程了,大家看图,一目了然。

有点需要注意,在 waiting 状态到 runnable 状态时,如果线程可以获得锁,那就正常晋级为 runnable,否则会进入到 blocked 状态。

操作系统级别的线程

操作系统级别的线程状态一共有 5 种,分别是新建、就绪、运行、阻塞、终结。这里对应的 Java 线程状态有一个对应关系如图所示。

线程状态对应关系

这里必须要吐槽一句,很多文章中都会把这些状态搞混,花在一张图上,要知道这是不同的层面上对线程的描述呀。

操作系统级别的线程阻塞可能包含的场景有 阻塞 IO、blocked、waiting、timed waiting。

Java 中的 runnable 可能对应于操作系统中的就绪|运行|阻塞 IO 状态。

Java 中创建线程

这是比较基础的点哈,为了完整性补充说明一下,主要有两点,第一是继承 Thread 类,实现 run 方法,第二种方法是实现 Runnable 接口,实现 run 方法,这里就有一点要注意,run 方法中只是这个线程需要执行的业务逻辑,而真正启动线程还需要我们的主函数调用 Thread 的 start 方法,这样线程就进入了我们上面说到的新建状态。

但是,我们真的要使用多线程的时候呢,很少会独立的创建一个线程,大部分情况下会考虑使用线程池。

线程池

阿里的开发规范中有一条就明确说了,要使用线程必须从线程池中获得,为什么这样呢?

使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者线程"过度切换"的问题。

我们使用线程池主要的类就是 ThreadPoolExecutor,而不管我们怎么调用,最终都会到这么一个构造函数,我们一起来分析一下每个参数代表的意思。

    /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
复制代码

corePoolSize: 核心线程数,最多保留的线程数。

maximumPoolSize: 最大线程数目,核心线程数 + 救急线程数

keepAliveTime:救急线程的生存时间

unit:针对救急线程的时间单位

workQueue:阻塞队列

threadFactory:线程工厂,可以为线程创建时起个好名字

handler:拒绝策略,一共有四种,分别是 丢弃抛异常 | 丢弃 | 丢弃队头的任务 | 由调用者处理该 Task

ThreadPoolExecutor 运行流程

首先核心线程执行 task, 更多的 task 来了之后会放在 workQueue 中,当 workQueue 满了之后,会唤起救急线程执行来的 task, 救急线程执行完 task 之后,会去执行 workQueue 中的 task,在 keepAliveTime 内没有新的任务进入到 workQueue 中,救急线程就会从线程池中释放。若是救急线程使用完了还是有新的 task 过来,就会根据 handler 拒绝策略做出反应。

这里一定要注意,不是说核心线程使用完就立马上救急线程!!!还有一点就是 workQueue 一定要指定长度,不然可能会内存溢出呦

ThreadPoolExecutor运行流程

总结

以上就是本文的所有内容了,主要说了不同层面的线程的差别,还有 Java 中线程的定义和使用,线程池的运行流程和注意事项。

除非不适用多线程,使用就用线程池,除非不用线程池,使用就需要设置阻塞队列的长度。