专栏首页汤圆学JavaJava并发-线程池篇-附场景分析
原创

Java并发-线程池篇-附场景分析

作者:汤圆

个人博客:javalover.cc

前言

前面我们在创建线程时,都是直接new Thread();

这样短期来看是没有问题的,但是一旦业务量增长,线程数过多,就有可能导致内存异常OOM,CPU爆满等问题

幸运的是,Java里面有线程池的概念,而线程池的核心框架,就是我们今天的主题,Executor

接下来,就让我们一起畅游在Java线程池的海洋中吧

本节会用银行办业务的场景来对比介绍线程池的核心概念,这样理解起来会很轻松

简介

Executor是线程池的核心框架;

和它相对应的有一个辅助工厂类Executors,这个类提供了许多工厂方法,用来创建各种各样的线程池,下面我们先看下几种常见的线程池

// 容量固定的线程池
Executor fixedThreadPool = Executors.newFixedThreadPool(5);
// 容量动态增减的线程池
Executor cachedThreadPool = Executors.newCachedThreadPool();
// 单个线程的线程池
Executor singleThreadExecutor = Executors.newSingleThreadExecutor();
// 基于调度机制的线程池(不同于上面的线程池,这个池创建的任务不会立马执行,而是定期或者延时执行)
Executor scheduledThreadPool = Executors.newScheduledThreadPool(5);

上面这些线程池的区别主要就是线程数量的不同以及任务执行的时机

下面让我们开始吧

文章如果有问题,欢迎大家批评指正,在此谢过啦

目录

  1. 线程池的底层类ThreadPoolExecutor
  2. 为啥阿里不建议使用 Executors来创建线程池?
  3. 线程池的生命周期 ExecutorService

正文

1. 线程池的底层类 ThreadPoolExecutor

在文章开头创建的几个线程池,内部都是有调用ThreadPoolExecutor这个类的,如下所示

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

这个类是Exexutor的一个实现类,关系图如下所示:

  • 其中Executors就是上面介绍的辅助工厂类,用来创建各种线程池
  • 接口ExecutorService是Executor的一个子接口,它对Executor进行了扩展,原有的Executor只能执行任务,而ExecutorService还可以管理线程池的生命周期(下面会介绍)

所以我们先来介绍下这个底层类,它的完整构造参数如下所示:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {

在介绍这些参数之前,我们可以先举个生活中的例子-去银行办业务;然后对比着来理解,会比较清晰

(图中绿色的窗口表示一直开着)

  • corePoolSize: 核心线程数,就是一直存在的线程(不管用不用);=》窗口的1号窗和2号窗
  • maximumPoolSize:最大线程数,就是最多可以创建多少个线程;=》窗口的1,2,3,4号窗
  • keepAliveTime:多余的线程(最大线程数 减去 核心线程数)空闲时存活的时间;=》窗口的3号窗和4号窗空闲的时间,如果超过keepAliveTime,还没有人来办业务,那么就会暂时关闭3号窗和4号窗
  • workQueue: 工作队列,当核心线程数都在执行任务时,再进来的任务就会添加到工作队列中;=》椅子,客户等待区
  • threadFactory:线程工厂,用来创建初始的核心线程,下面会有介绍;
  • handler:拒绝策略,当所有线程都在执行任务,且工作队列也满时,再进来的任务就会被执行拒绝策略(比如丢弃);=》左下角的那个小人

基本的工作流程如下所示:

上面的参数我们着重介绍下工作队列和拒绝策略,线程工厂下面再介绍

工作队列:

  • ArrayBlockingQueue:
    • 数组阻塞队列,这个队列是一个有界队列,遵循FIFO,尾部插入,头部获取
    • 初始化时需指定队列的容量 capacity
    • 类比到上面的场景,就是椅子的数量为初始容量capacity
  • LinkedBlockingQueue:
    • 链表阻塞队列,这是一个无界队列,遵循FIFO,尾部插入,头部获取
    • 初始化时可不指定容量,此时默认的容量为Integer.MAX_VALUE,基本上相当于无界了,此时队列可一直插入(如果处理任务的速度小于插入的速度,时间长了就有可能导致OOM)
    • 类比到上面的场景,就是椅子的数量为Integer.MAX_VALUE
  • SynchronousQueue:
    • 同步队列,阻塞队列的特殊版,即没有容量的阻塞队列,随进随出,不做停留
    • 类比到上面的场景,就是椅子的数量为0,来一个人就去柜台办理,如果柜台满了,就拒绝
  • PriorityBlockingQueue
    • 优先级阻塞队列,这是一个无界队列,不遵循FIFO,而是根据任务自身的优先级顺序来执行
    • 初始化可不指定容量,默认11(既然有容量,怎么还是无界的呢?因为它添加元素时会进行扩容)
    • 类比到上面的场景,就是新来的可以插队办理业务,好比各种会员

拒绝策略:

  • AbortPolicy(默认):
    • 中断策略,抛出异常 RejectedExecutionException;
    • 如果线程数达到最大,且工作队列也满,此时再进来任务,则抛出 RejectedExecutionException(系统会停止运行,但是不会退出)
  • DiscardPolicy:
    • 丢弃策略,丢掉新来的任务
    • 如果线程数达到最大,且工作队列也满,此时再进来任务,则直接丢掉(看任务的重要程度,不重要的任务可以用这个策略)
  • DiscardOldestPolicy:
    • 丢弃最旧策略,丢掉最先进入队列的任务(有点残忍了),然后再次执行插入操作
    • 如果线程数达到最大,且工作队列也满,此时再进来任务,则直接丢掉队列头部的任务,并再次插入任务
  • CallerRunsPolicy:
    • 回去执行策略,让新来的任务返回到调用它的线程中去执行(比如main线程调用了executors.execute(task),那么就会将task返回到main线程中去执行)
    • 如果线程数达到最大,且工作队列也满,此时再进来任务,则直接返回该任务,到调用它的线程中去执行

2. 为啥阿里不建议使用 Executors来创建线程池?

原话如下:

我们可以写几个代码来测试一下

先测试FixedThreadPool,代码如下:

public class FixedThreadPoolDemo {
    public static void main(String[] args) {
        // 创建一个固定容量为10的线程池,核心线程数和最大线程数都为10
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1_000_000; i++) {
            try{
                executorService.execute(()->{
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

这里我们需对VM参数做一点修改,让问题比较容易复现

如下所示,我们添加-Xmx8m -Xms8m到VM option中(-Xmx8m:JVM堆的最大内存为8M, -Xms8m,JVM堆的初始化内存为8M):

此时点击运行,就会发现报错如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
    at com.jalon.concurrent.chapter6.FixedThreadPoolDemo.main(FixedThreadPoolDemo.java:21)

我们来分析下原因

  • 首先,newFixedThreadPool内部用的工作队列为LinkedBlockingQueue,这是一个无界队列(容量最大为Integer.MAX_VALUE,基本上可一直添加任务)
  • 如果任务插入的速度,超过了任务执行的速度,那么队列肯定会越来越长,最终导致OOM

CachedThreadPool也是类似的原因,只不过它是因为最大线程数为Integer.MAX_VALUE; 所以当任务插入的速度,超过了任务执行的速度,那么线程的数量会越来越多,最终导致OOM

那我们要怎么创建线程池呢?

可以用ThreadPoolExecutor来自定义创建,通过为最大线程数和工作队列都设置一个边界,来限制相关的数量,如下所示:

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        ExecutorService service = new ThreadPoolExecutor(
                1, // 核心线程数
                1, // 最大线程数
                60L, // 空闲时间
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(1), // 数组工作队列,长度1
                new ThreadPoolExecutor.DiscardPolicy()); // 拒绝策略:丢弃
        for (int i = 0; i < 1_000_000; i++) {
            // 通过这里的打印信息,我们可以知道循环了3次
            // 原因就是第一次的任务在核心线程中执行,第二次的任务放到了工作队列,第三次的任务被拒绝执行
            System.out.println(i);
            service.execute(()->{
                // 这里会报异常,是因为执行了拒绝策略(达到了最大线程数,队列也满了,此时新进来的任务就会执行拒绝策略)
                // 这里需要注意的是,抛出异常后,代码并不会退出,而是卡在异常这里,包括主线程也会被卡住(这个是默认的拒绝策略)
                // 我们可以用其他的拒绝策略,比如DiscardPolicy,此时代码就会继续往下执行
                System.out.println(Thread.currentThread().getName());
            });
        }
        try {
            Thread.sleep(1000);
            System.out.println("主线程 sleep ");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3. 线程池的生命周期 ExecutorService

Executor接口默认只有一个方法void execute(Runnable command);,用来执行任务

任务一旦开启,我们就无法再去插手了,比如停止、监控等

此时就需要ExecutorService登场了,它是Executor的一个子接口,对其进行了扩展,方法如下:

public interface ExecutorService extends Executor {
​
    void shutdown(); // 优雅地关闭,这个关闭会持续一段时间,以等待已经提交的任务去执行完成(但是在shutdown之后提交的任务会被拒绝)
​
    List<Runnable> shutdownNow(); // 粗暴地关闭,这个关闭会立即关闭所有正在执行的任务,并返回工作队列中等待的任务
​
    boolean isShutdown();
​
    boolean isTerminated();
​
    // 用来等待线程的执行
    // 如果在timeout之内,线程都执行完了,则返回true;
    // 如果等了timeout,还没执行完,则返回false;
    // 如果timeout之内,线程被中断,则抛出中断异常
    boolean awaitTermination(long timeout, TimeUnit unit) 
        throws InterruptedException;
​
    <T> Future<T> submit(Callable<T> task);
   
    <T> Future<T> submit(Runnable task, T result);
}

从上面可以看到,线程池的生命周期分三步:

  1. 运行:创建后就开始运行
  2. 关闭:调用shutdown进入关闭状态
  3. 已终止:所有线程执行完毕

总结

  1. 线程池的底层类 ThreadPoolExecutor:核心概念就是核心线程数、最大线程数、工作队列、拒绝策略
  2. 为啥阿里不建议使用 Executors来创建线程池?:因为会导致OOM,解决办法就是自定义ThreadPoolExecutor,为最大线程数和工作队列设置边界
  3. 线程池的生命周期ExecutorService:运行状态(创建后进入)、关闭状态(shutdown后进入)、已终止状态(所有线程都执行完成后进入)

参考内容:

后记

愿你的意中人亦是中意你之人

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 转架构师你需要准备哪些面试题目

    2019.05.26 北京昨天下了一天雨,今天更是很凉爽,下午下楼喂喂野猫出去溜达一圈,回来给网友整理了一些面试题,知识点大概覆盖百分之八十了,...

    王炸
  • 2019年末Java面试解析总结:Java+Redis+数据库+解决方案+分布式..

    2019年快结束了,给大家整理了今年来最经典的面试真题100道,每个题目都有详细的解答,收集了java基础、RabbitMQ,微服务、MySQL数据库、Java...

    用户5546570
  • Java 应用线上问题排查思路、工具小结

    本文总结了一些Java应用线上常见问题的定位步骤,分享的主要目的是想让对线上问题接触少的同学有个预先认知,免得在遇到实际问题时手忙脚乱。毕竟作者自己也是从手忙脚...

    Spark学习技巧
  • Java开发之Java线程池原理分析学习

    线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控

    用户1289394
  • Java并发编程之美

    并发编程相比 Java 中其他知识点的学习门槛较高,从而导致很多人望而却步。但无论是职场面试,还是高并发/ 高流量系统的实现,却都离不开并发编程,于是能够真正掌...

    加多
  • 编写高性能 Java 代码的最佳实践

    在这篇文章中,我们将讨论几个有助于提升Java应用程序性能的方法。我们首先将介绍如何定义可度量的性能指标,然后看看有哪些工具可以用来度量和监控应用程序性能,以及...

    哲洛不闹
  • 后台开发常问面试题集锦(问题搬运工,文末附问题链接)

    Synchronized(对象锁)和Static Synchronized(类锁)的区别

    Java架构技术
  • 狗哥肝了一下午的线程池,奉上~

    对线程还没有概念的小伙伴建议看看,哪里写错了,或写的不好的?给我提一些宝贵建议。感谢,今天进入线程的第六篇,线程池的学习。

    一个优秀的废人
  • 如何提升Java应用程序性能

    【摘要】本文首先介绍了负载测试、基于APM工具的应用程序和服务器监控,随后介绍了编写高性能Java代码的一些最佳实践。最后研究了JVM特定的调优技巧、数据库端的...

    CSDN技术头条
  • 如何提升Java应用程序性能

    作者 | Eugen Paraschiv 翻译 | 雁惊寒 【摘要】本文首先介绍了负载测试、基于APM工具的应用程序和服务器监控,随后介绍了编写高性能Java代...

    企鹅号小编
  • 编写高性能 Java 代码的最佳实践

    摘要:本文首先介绍了负载测试、基于APM工具的应用程序和服务器监控,随后介绍了编写高性能Java代码的一些最佳实践。最后研究了JVM特定的调优技巧、数据库端的优...

    lyb-geek
  • 幕后英雄的用武之地——浅谈Java内部类的四个应用场景

    ——浅谈Java内部类的四个应用场景

    林老师带你学编程
  • 【Java并发编程】从源码分析几道必问线程池的面试题?

    上一篇文章我们有介绍过线程池的一个基本执行流程《【Java并发编程】面试必备之线程池》以及它的7个核心参数,以及每个参数的作用、以及如何去使用线程池 还留了几个...

    java金融
  • java高并发系列 - 第18天:JAVA线程池,这一篇就够了

    大家用jdbc操作过数据库应该知道,操作数据库需要和数据库建立连接,拿到连接之后才能操作数据库,用完之后销毁。数据库连接的创建和销毁其实是比较耗时的,真正和业务...

    路人甲Java
  • 寒冬也挡不住进大厂的决心,Android面试完整知识架构,面试需要掌握的都在这里!

    年年寒冬,年年也挡不住一个Android程序员追求大厂的决心。想要进入大厂,我们需要掌握哪些知识点呢?这里,我为大家梳理了一个整体的知识架构。整体包括Java、...

    Android技术干货分享
  • 从火箭发场景来学习Java多线程并发闭锁对象

    在我们开发过程中,有时候会使用到倒计时计数器。最简单的是:int size = 5; 执行后,size—这种方式来实现。但是在多线程并发的情况下,这种操作会不安...

    凯哥Java
  • 个人珍藏的80道多线程并发面试题(11-20答案解析)

    个人珍藏的80道Java多线程/并发经典面试题,现在给出11-20的答案解析哈,并且上传github哈~

    捡田螺的小男孩
  • 突然就懵了!面试官问我:线程池中多余的线程是如何回收的?

    公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。

    java进阶架构师
  • 那些年,我们追过的“定时调度”

    定时调度 作为MadPecker的后端开发人员,我们总会遇到这样的业务场景:每周同步一批数据;每半个小时检查一遍服务器运行状况;每天早上八点给用户发送一份包含今...

    MadPecker

扫码关注云+社区

领取腾讯云代金券