线程池的应用及工作原理

一、什么是线程池?

        线程池是一种多线程处理形式,它预先创建一组线程并维护他们,以便在需要执行异步任务时可以立即使用,而不必每次都创建新线程,可以资源复用;

二、为什么要使用线程池?

  1. 减少重复创建与销毁线程,复用已有线程;(这也是池化技术出现的原因,比如数据库连接池,线程池等,都是为了避免重复创建与复用)

  2. 提高响应速度,任务到达时线程已经存在,无需等待线程的创建;

  3. 可以控制并发线程的数量,提高线程可管理性;

  4. 避免无限制创建线程从而导致系统崩溃,防止资源耗尽;(不使用线程池,频繁的创建线程可能会导致程序的崩溃,即便是在程序中偶尔创建1~2个线程,当该任务的并发量特变大的时候也可能会造成系统的崩溃,所以说使用线程池是一种更合理和更高效的选择)

三、线程池的具体应用

1、创建ThreadPoolExcutor实例,并指定相关参数;

ThreadPoolExcutor tpe = new ThreadPoolExcutor(

     corePoolSize,           //①核心线程数,即便处于闲置状态也不会被回收;

     maximumPoolSize,  //②线程池中允许的最大的线程数(核心线程+临时线程)

     keepAliveTime,        //③非核心线程存活时间,非核心线程处于空闲超过指定时间则被回收;

     keepAliveTimeUnit, //④keepAliveTime的时间单位TimeUnit.SECONDS、MINUTES等;

     workQueue,            //⑤线程池中用于存储任务的队列,有无界队列、有界队列等;

     线程工厂,                //⑥用于创建线程的工程类,一般使用默认的即可;

     拒绝策略)                  //⑦当没有空闲的线程,且任务队列满后采用的策略;


ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        2,
        10,
        10,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1024),      //有界队列,大小为1024;
        Executors.defaultThreadFactory(),        //默认工厂;
        new ThreadPoolExecutor.AbortPolicy() //直接抛出异常;
);

2、线程池的拒绝策略

        当(核心线程 + 临时线程) >= 最大核心线程池数,且任务队列也满后,再次提交任务时,触发的策略,被称为拒绝策略;线程池中有默认的策略供我们选择,也可自定义;

1. AbortPolicy:直接抛出异常,默认策略 【能让开发人员及时发现问题,并及时解决】

2. DiscardPolicy:丢弃任务,不处理 【无法发现系统问题,可能会导致一些问题无法解决,建议在一些无关紧要的任务中使用】

3. DiscardOldestPolicy:丢弃队列中最旧的任务,尝试执行新任务【会直接丢弃,建议在一些无关紧要的任务中使用】

4. CallerRunsPolicy:由调用者线程来执行任务【比如是main调用的,那就由main线程来执行,不会导致任务丢失】

5. 自定义策略,实现RejectedExecutionHandler接口,并实现rejectedExecution()方法,在该方法中编写自定义拒绝策略的逻辑;

3、利用线程池执行任务;

① ThreadPoolExecutor实例.execute(任务)【无返回值】

② ThreadPoolExecutor实例.submit(任务)  【有返回值,返回Future对象,可获取结果】


示例①

threadPoolExecutor.execute(() -> {

    System.out.println("我是通过ThreadPoolExecutor创建的线程!");
    //....任务代码
});

示例②

Future<String> submit = threadPoolExecutor.submit(() -> {
     System.out.println("我是通过ThreadPoolExecutor创建的线程!");
     //....任务代码
     return "返回值";
});

try {
     submit.get(); // 获取线程执行结果;
} catch (Exception e) {
     throw new RuntimeException(e);
}

4、线程池使用补充

①  默认情况下,核心线程不会被回收,如果需要回收核心线程,通过以下配置后,即便是核心线程处于空闲状态也会被回收:

  • threadPoolExecutor.allowCoreThreadTimeOut(true);

② 默认情况下,创建线程池后不会马上自动创建线程,而是需要提交任务后才会创建线程;如果想创建线程池时就创建线程,可以使用以下两种方式:

  • threadPoolExecutor.prestartCoreThread();(创建线程池同时创建一个核心线程)
  • threadPoolExecutor.prestartAllCoreThreads();(创建线程池同时创建好所有核心线程)

③ 如何正确的关闭线程池

  • threadPoolExecutor.shutdownNow():停止接收新的任务,将正在执行的任务中断,忽略队列里面待执行的任务,返回没有执行的任务集合;

  • threadPoolExecutor.shutdown():停止接受新的任务,内部正在执行的任务和队列里面的任务会继续执行直到执行完毕,等待所有任务都执行完后才会关闭线程池;

  • threadPoolExecutor.awaitTermination(时间,单位):判断线程池是否关闭,已关闭返回true,否则阻塞传入的指定时间再次判断是否关闭;

        优雅关闭线程池示例👇:

        executor.shutdown();  //1、关闭线程池;

        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { //2、判断是否关闭成功;

                executor.shutdownNow(); //3、超过时间未关闭成功,强制关闭;

        }

④ 线程池的扩展方法

        自定义一个类继承ThreadPoolExecutor,然后实现里面的方法:

  • beforeExecute():线程任务执行前调用的方法,可以在此编写需要执行的业务逻辑;

  • afterExecute():任务执行结束后调用的方法,可以在此编写需要执行的业务逻辑;

  • terminated():线程池完全终止后调用的方法,可以在此编写需要执行的业务逻辑;

四、线程池的工作原理

        ①首先,创建线程池并指定相关参数;②然后当我们使用execute或submit方法向线程池提交任务;③如果线程池中有核心线程则直接分配线程去执行任务,否则会创建核心线程;④直到达到最大核心线程数量,再次提交的任务会被放到任务队列里面;⑤当核心线程没有空闲的、任务队列也已经满了之后,则会创建临时线程去执行异步任务;⑥当临时线程 + 核心线程数达到最大线程数量,且任务队列也已经满了之后,再次提交的任务就会触发拒绝策略;如图:

五、补充

1、合理设置线程池大小
  • 公式 ①:CPU的核心数 * CPU的使用率(0% - 100%) * (1 + (线程等待时间(60%)/ 线程计算时间(40%)))
  • 公式 ②:CPU的核心数 * (1 - 阻塞系数)

【其中阻塞系数为0.1 ~ 0.9,表示有多少时间是处于等待状态】比如0.4表示有40%的时间处于等待,比如IO等待,网络请求等待,60%的时间是CPU真正的在运行,根据任务的具体业务来判断;

        根据以上两个公式,国内也采用了一些方案,在Java代码中可以使用:获取该服务器的可用核心线程数量,方便我们动态的获取然后创建;

Java代码中获取服务器核心线程数:Runtime.getRuntime().availableProcessors();

  • 1、CPU密集型任务:线程数 = CPU核心线程数 + 1
  • 2、I/O密集型任务:线程数 = CPU核心线程数 * 2
  • 3、其他通用型:线程数 = ((线程等待时间 + 线程CPU运行时间)/ 线程CPU运行时间)* CPU核心数量
2、正确创建线程池     

不要在被经常调用的方法、多列类里创建线程池;

原因:因为线程池里面的核心线程默认不会被销毁,当频繁创建线程池会内积占用内存资源,最终导致内存溢出;

不在方法里创建线程池,而是作为类的成员或静态变量,但也要显式的调用shutdown(),否则类创建的频率高时也会出现内存溢出的问题;

优点:"特定场景",资源隔离,可以避免全局竞争,关键的业务独享一个线程池,可以根据不同的场景创建不同的线程池;

使用Spring的@Bean注解将线程池实例交给IOC容器管理,全局共享线程池,需要使用时注入即可;

优点:"通用场景",适用于全局任务,简单异步处理,创建后不需要手工维护(也可以根据不同业务配置多个线程池,都交给IOC容器管理)

注:如果非要在方法里面创建,可以将核心线程设置为可以被销毁,或使用后将线程池的资源释放也可以解决这样的问题;(不推荐)

 3、创建线程池阿里规范

        不能使用Executors工具类去创建线程池,因为它允许的队列长度、线程数量过大,且不能改变;可能导致OOM;而是通过new ThreadPoolExecutor()并指明相关参数的方式去创建,控制核心线程数,最大线程数,队列数量,拒绝策略等信息;

4、线程池的监控

        应用场景:对线程池的特定参数进行监控,将这些数据持久化。当程序运行过程中出现问题时,便于我们去排查和定位问题;

实现方式:定时采集线程池运行时的数据,并将这些数据持久化,便于后续查看然后定位问题;

具体实现:

① 创建一张用于存储线程池运行时所产生的数据;

② 编写定时任务,定时收集线程运行相关数据,然后存入数据库里;

注:详细示例可以自行查阅;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值