线程池的五种状态及创建线程池的几种方式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程池的五种状态及创建线程池的几种方式相关的知识,希望对你有一定的参考价值。

?技术图片

上篇《Java线程的6种状态详解及创建线程的4种方式
前言:我们都知道,线程是稀有资源,系统频繁创建会很大程度上影响服务器的使用效率,如果不加以限制,很容易就会把服务器资源耗尽。所以,我们可以通过创建线程池来管理这些线程,提升对线程的使用率。

1、什么是线程池?

简而言之,线程池就是管理线程的一个容器,有任务需要处理时,会相继判断核心线程数是否还有空闲、线程池中的任务队列是否已满、是否超过线程池大小,然后调用或创建线程或者排队,线程执行完任务后并不会立即被销毁,而是仍然在线程池中等待下一个任务,如果超过存活时间还没有新的任务就会被销毁,通过这样复用线程从而降低开销。

2、使用线程池有什么优点?

可能有人就会问了,使用线程池有什么好处吗?那不用说,好处自然是有滴。大概有以下:
1、提升线程池中线程的使用率,减少对象的创建、销毁。
2、线程池的伸缩性对性能有较大的影响,使用线程池可以控制线程数,有效的提升服务器的使用资源,避免由于资源不足而发生宕机等问题。(创建太多线程,将会浪费一定的资源,有些线程未被充分使用;销毁太多线程,将导致之后浪费时间再次创建它们;创建线程太慢,将会导致长时间的等待,性能变差;销毁线程太慢,导致其它线程资源饥饿。)

3、线程池的核心工作流程(重要)

我们要使用线程池得先了解它是怎么工作的,流程如下图,废话不多说看图就行。核心就是复用线程,降低开销。
技术图片

4、线程池的五种状态生命周期

  • RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务。
  • SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown() 方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用 shutdown() 方法进入该状态)。
  • STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。
  • TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入 TERMINATED 状态。
  • TERMINATED:在 terminated() 方法执行完后进入该状态,默认 terminated() 方法中什么也没有做。
    技术图片

    5、创建线程池的几种方式

  • 通过 Executors 工厂方法创建
  • 通过 new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) 自定义创建
    相对而言,更建议用第二个创建线程池,Executors 创建的线程池内部很多地方用到了***任务队列,在高并发场景下,***任务队列会接收过多的任务对象,严重情况下会导致 JVM 崩溃,一些大厂也是禁止使用 Executors 工厂方法去创建线程池。newFixedThreadPool 和 newSingleThreadExecutor 的主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM;newCachedThreadPool 和 newScheduledThreadPool 的主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

    5.1、Executors 五个工厂方法创建不同线程池的区别

    技术图片
    1、newCachedThreadPool()(工作队列使用的是 SynchronousQueue)
    创建一个线程池,如果线程池中的线程数量过大,它可以有效的回收多余的线程,如果线程数不足,那么它可以创建新的线程。
    不足:这种方式虽然可以根据业务场景自动的扩展线程数来处理我们的业务,但是最多需要多少个线程同时处理却是我们无法控制的。
    优点:如果当第二个任务开始,第一个任务已经执行结束,那么第二个任务会复用第一个任务创建的线程,并不会重新创建新的线程,提高了线程的复用率。
    作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。
    2、newFixedThreadPool()(工作队列使用的是 LinkedBlockingQueue)
    这种方式可以指定线程池中的线程数。如果满了后又来了新任务,此时只能排队等待。
    优点:newFixedThreadPool 的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器达到最大的使用率,同时又可以保证即使流量突然增大也不会占用服务器过多的资源。
    作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。
    3、newScheduledThreadPool()
    该线程池支持定时,以及周期性的任务执行,我们可以延迟任务的执行时间,也可以设置一个周期性的时间让任务重复执行。该线程池中有以下两种延迟的方法。
    scheduleAtFixedRate 不同的地方是任务的执行时间,如果间隔时间大于任务的执行时间,任务不受执行时间的影响。如果间隔时间小于任务的执行时间,那么任务执行结束之后,会立马执行,至此间隔时间就会被打乱。
    scheduleWithFixedDelay 的间隔时间不会受任务执行时间长短的影响。
    作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。
    4、newSingleThreadExecutor()
    这是一个单线程池,至始至终都由一个线程来执行。
    作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按 FIFO 方式顺序执行任务队列中的任务。
    5、newSingleThreadScheduledExecutor()
    只有一个线程,用来调度任务在指定时间执行。
    作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为 1,而上面的可以指定线程池的大小。
    使用示例:

    //创建一个会根据需要创建新线程的线程池
    ExecutorService executor= Executors.newCachedThreadPool();
    for (int i = 0; i < 20; i++) {
    executor.submit(new Runnable() {
        @Override
        public void run() {
            System.out.println(i);
        }
    });
    }

    这五种线程池都是直接或者间接获取的 ThreadPoolExecutor 实例 ,只是实例化时传递的参数不一样。所以如果 Java 提供的线程池满足不了我们的需求,我们可以通过 ThreadPoolExecutor 构造方法创建自定义线程池。

    5.2、ThreadPoolExecutor 构造方法参数详解

public ThreadPoolExecutor(
int corePoolSize,//线程池核心线程大小
int maximumPoolSize,//线程池最大线程数量
long keepAliveTime,//空闲线程存活时间
TimeUnit unit,//空闲线程存活时间单位,一共有七种静态属性(TimeUnit.DAYS天,TimeUnit.HOURS小时,TimeUnit.MINUTES分钟,TimeUnit.SECONDS秒,TimeUnit.MILLISECONDS毫秒,TimeUnit.MICROSECONDS微妙,TimeUnit.NANOSECONDS纳秒)
BlockingQueue<Runnable> workQueue,//工作队列
ThreadFactory threadFactory,//线程工厂,主要用来创建线程(默认的工厂方法是:Executors.defaultThreadFactory()对线程进行安全检查并命名)
RejectedExecutionHandler handler//拒绝策略(默认是:ThreadPoolExecutor.AbortPolicy不执行并抛出异常)
) 

使用示例:

ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 20, 2, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5));

5.2.1、工作队列

jdk 中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按 FIFO 排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到 corePoolSize 后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到 maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的***阻塞队列(其实最大容量为 Interger.MAX_VALUE),按照 FIFO 排序。由于该队列的近似***性,当线程池中线程数量达到 corePoolSize 后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到 maxPoolSize,因此使用该工作队列时,参数 maxPoolSize 其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到 maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的***阻塞队列,优先级通过参数 Comparator 实现。

5.2.2、拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,就会执行拒绝策略。jdk中提供了4中拒绝策略:
①ThreadPoolExecutor.CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的 run 方法,除非线程池已经 shutdown,则直接抛弃任务。
②ThreadPoolExecutor.AbortPolicy
该策略下,直接丢弃任务,并抛出 RejectedExecutionException 异常。
③ThreadPoolExecutor.DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④ThreadPoolExecutor.DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
除此之外,还可以根据应用场景需要来实现 RejectedExecutionHandler 接口自定义策略。

6、线程池的关闭

  • shutdown():
    1、调用之后不允许继续往线程池内添加线程;
    2、线程池的状态变为 SHUTDOWN 状态;
    3、所有在调用 shutdown() 方法之前提交到 ExecutorSrvice 的任务都会执行;
    4、一旦所有线程结束执行当前任务,ExecutorService 才会真正关闭。
  • shutdownNow():
    1、该方法返回尚未执行的 task 的 List;
    2、线程池的状态变为 STOP 状态;
    3、尝试停止所有的正在执行或暂停任务的线程。
    简单点来说,就是:
    shutdown() 调用后,不可以再 submit 新的 task,已经 submit 的将继续执行
    shutdownNow() 调用后,试图停止当前正在执行的 task,并返回尚未执行的 task 的 list

    7、总结

    本文简单介绍了线程池的一些相关知识,相信大家对线程池的优点,线程池的生命周期,线程池的工作流程及线程池的使用有了一个大概的了解,也希望能对有需要的人提供一点帮助!文中有错误的地方,还请留言给予指正,谢谢~
    也欢迎大家关注我的公众号:Java的成神之路,免费领取最新面试资料,技术电子书,架构进阶相关资料等。
    技术图片

以上是关于线程池的五种状态及创建线程池的几种方式的主要内容,如果未能解决你的问题,请参考以下文章

关于线程池的五种实现方式,附答案

关于线程池的五种实现方式,隔壁都馋哭了

关于线程池的五种实现方式,冲刺7天拿下Offer!

Java多线程和并发,Java线程池

自定义线程池的几种方案

深入理解 Java 线程池的实现原理