【Java学习笔记】线程池详解:从理解到应用

想要了解的部分

  • 线程池创建的七大参数
  • 如何创建线程池
  • 拒绝策略如何选择
  • 什么场景需要使用什么线程池
  • 如何使用线程池
  • 为什么阿里规范不能使用Executors创建线程池?

参考资料+ 图灵学院的直播公开课

文章目录

01 线程的七大参数

翻看源码可以看到线程池对象的构造函数如下:

public ThreadPoolExecutor(int corePoolSize,	// 核心线程数
                          int maximumPoolSize,	// 最大线程数
                          long keepAliveTime,	// 线程可存活时间
                          TimeUnit unit,		// 时间单位
                          BlockingQueue<Runnable> workQueue,	// 阻塞队列
                          ThreadFactory threadFactory,	// 线程工厂
                          RejectedExecutionHandler handler	// 拒绝策略
                         ) {

同时比较常用的可以看到5参数版的构造函数:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
  • 线程公司使用了默认的,如下

    new DefaultThreadFactory()
    
  • 拒绝策略也指向了默认的,如下

    RejectedExecutionHandler defaultHandler = new AbortPolicy();
    

线程池内各个线程执行的顺序

七大参数之间的关系看这个视频

关于执行的顺序,涉及到两个概念 【提交优先级】和【执行优先级】

1)提交优先级

简单理解,当一个新的线程被提交时,会优先提交给核心线程,然后是队列,队列不足时才会创建新的线程(直到最大线程数)

可以查看源码 ,出处

image-20230322114010471

看图

image-20230322113842706

2)执行优先级

但是在执行线程池,顺序就不一样了,优先执行核心线程,再执行临时线程,最后再执行队列中的线程

02 如何创建线程池

先了解一下Executors提供的几种常用的线程池配置:

image-20230322104441120

一、newCachedThreadPool (纯缓存线程池)

源码如下:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • 核心线程为0,而最大线程为无限(几乎) ——所以的线程都是临时搭建,超过一定的时间后,就会自动销毁
  • 使用的队列 是:SynchronousQueue (翻译过来叫同步队列

优点:

  • 拥有最快的速度,调用了最多的线程

缺点:

  • 创建多线程会占用CPU资源,会导致CPU占用过高!

二、newFixedThreadPool (固定大小的线程池)

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • 核心线程数 = 最大线程数 = 创建时传入的int大小
    • 表示没有临时线程
  • 使用的是一个链表队列LinkedBlockingQueue

优点:

  • 速度适中,占用的CPU资源固定

缺点:

  • 大量的任务会导致内存溢出!因为超出的线程都会占用内存空间

三、newSingleThreadExecutor(单一线程池)

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • 核心线程数固定为1 的 FixedThreadPool

优点:

  • CPU占用低

缺点:

  • 和fixed一样,内容可能会溢出
⭐️ 衍生问题:线程池的队列Queue是如何选择的?
(1)SynchronousQueue

(API)一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有.

简单理解:在用不到队列的使用,选择该队列

(2)LinkedBlockingQueue

表示无限长的队列,所以FixedThreadPool中即使没有临时线程,超出部分的线程就会全部存入内存中进行等待

简单理解:在大量使用队列时,选择该队列

使用该队列有一个极大的风险:存在内容溢出问题

(3)ArrayBlockingQueue

(API) 这是一个典型的**“有界缓存区”**,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素

可以声明长度大小的队列,声明时必须传入长度int

简单理解:比较普遍情况使用的队列,一般可以用于自定义声明线程池时使用。放置内容溢出。

03 拒绝策略如何选择?

一、拒绝策略是什么时候触发?

image-20230322113842706

从前面的提交优先级分析就可以看到,只有当提交任务出现【核心线程、队列、临时线程】都满了的情况时,就会触发拒绝策略

二、拒绝策略的类型

目前所有的拒绝策略都实现于RejectedExecutionHandler接口

image-20230322114422970

实现类有以下四种拒绝策略

1)AbortPolicy (默认的流产策略)

永远都是直接抛出异常(一旦进入拒绝就抛出异常),该任务将会直接被忽略跳过
image-20230322114010471

2)CallerRunsPolicy (调用者执行策略)

(api)用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。

简单理解:让线程池的调用者自己去执行这个线程的run()方法

aaa

测试一下:写一个简单的demo,不难发现当触发拒绝策略时,main线程执行了run方法

在这里插入图片描述

3)DiscardOldestPolicy (最老抛弃策略)

(api)用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试 execute;如果执行程序已关闭,则会丢弃该任务。

4)DiscardPolicy (静默抛弃原则)

(api)用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。

04 如何使用线程池

思路如下

  1. 创建线程池(前面已经提到
  2. 提交/执行任务(涉及到两个方法 excute和submit
  3. 销毁线程池 ( 涉及shutdown

一、两个方法 excute和submit的区别

img

img

两个方法都可以将任务提交给线程池进行管理并执行,区别在于:

  • execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。
  • execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。

二、衍生问题:如何处理callable线程的返回数据?

简单处理:

Callable的返回类型是Future,调用future.get()就可以实现异常的抛出了。实例如下

public class TestThreadPoolBegin {
    public static void main(String[] args) throws Exception{
        ExecutorService es = Executors.newSingleThreadExecutor();
        Callable callable = new Callable() {
            @Override
            public Object call() throws Exception {
                System.out.println("线程处理开始...");
                int a = 0;
                int b = 3;
                System.out.println("除以0的结果为:" + b/a);
                System.out.println("线程处理结束...");
                return "0";
            }
        };
        Future<String> future = es.submit(callable);
        System.out.println("任务执行完成,结果为:" + future.get());
        es.shutdown();
    }
}
更高级的处理

future的get方法在未获得返回值之前会一直阻塞,我们可以使用future的isDone方法判断任务是否执行完成,然后再决定是否get,因此上述代码我们可以优化如下:

public class TestThreadPoolBegin {

    public static void main(String[] args) throws Exception{
        ExecutorService es = Executors.newSingleThreadExecutor();
        Callable callable = new Callable() {
            @Override
            public String call() throws Exception {
                System.out.println("线程处理开始...");
                int a = 2;
                int b = 3;
                System.out.println("3/2的结果为:" + b/a);
                System.out.println("线程处理结束...");
                return "0";
            }
        };
        Future<String> future = es.submit(callable);
        while(true) {
            //idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。
            if(future.isDone()) {
                System.out.println("任务执行完成:" + future.get());
                break;
            }
        }
        es.shutdown();
    }
}
值得注意的是,经过测试,可以这么简单总结Callable的线程使用情况。
  1. 重写call()方法,并且返回类型就是回调参数的类型
  2. 在处理结束后返回对应的类型对象
  3. 启动该线程(使用自身的call并不是多线程,应该用线程池的submit进行启动调用)
  4. 对回调结果Future进行get()处理(相当于取获取线程的处理结果
    • 这里可以加一步isDone进行判断是否已完成

05 线程池的使用场景

一、取代原本多线程应用到的场景

比如:异步发邮件、心跳线程、或者是对效能优化的一些多线程场景

二、针对executors类生产的几种线程池的常用场景

参考地址

1、newCachedThreadPool

创建一个线程池,如果线程池中的线程数量过大,它可以有效的回收多余的线程,如果线程数不足,那么它可以创建新的线程。

**应用场景:**用于线程数不固定,但是启动时间间隔较长的场景,可以极大的复用线程。

2、newFixedThreadPool /

固定线程数量的线程池,线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器打到最大的使用率,同事又可以保证及时流量突然增大也不会占用服务器过多的资源。

应用场景:用于需要执行的线程数固定的场景,比如已经明确了n条任务并行的场景

newSingleThreadExecutor 同2
3、⭐️ newScheduledThreadPool (定时线程池)

该线程池支持定时,以及周期性的任务执行,我们可以延迟任务的执行时间,也可以设置一个周期性的时间让任务重复执行。以下特意详解该类型的线程池。

三、定时线程池详解 :ScheduledThreadPool (定时线程池)

1、创建

构造函数如下:

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory, handler);
}
  • 和原本的线程池对象ThreadPoolExecutor不同的是,最低参数要求只有一个:corePoolSize( - 池中所保存的线程数(包括空闲线程))。
  • 继承于线程池对象,所以super即使调用ThreadPoolExecutor的构造函数

2、使用

(api:ScheduledExecutorService)schedule方法使用各种延迟创建任务,并返回一个可用于取消或检查执行的任务对象。scheduleAtFixedRatescheduleWithFixedDelay 方法创建并执行某些在取消前一直定期运行的任务。

1)scheduleAtFixedRate (固定速度执行计划

image-20230322160011308

创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;

也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推。

如果任务的任何一个执行遇到异常,则后续执行都会被取消。否则,只能通过执行程序的取消或终止方法来终止该任务。

如果此任务的任何一个执行要花费比其周期更长的时间,则将推迟后续执行,但不会同时执行。写一个测试demo

package com.thread;

import java.text.DateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduleThreadDemo {

    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
        System.out.println("开始执行时间:" +
                        DateFormat.getTimeInstance().format(new Date()));
//        executor.scheduleAtFixedRate(new ThreadDemoS() , 1, 5, TimeUnit.SECONDS);
        executor.scheduleAtFixedRate(new ThreadDemoS() , 1, 5, TimeUnit.SECONDS);
    }

}

class ThreadDemoS extends Thread{
    private final Random random = new Random();
    private long[] sleeps = {10000l,5000l,1000l};
    @Override
    public void run() {
        long start = System.currentTimeMillis();
        System.out.println("scheduleAtFixedRate 开始执行时间:" +
                DateFormat.getTimeInstance().format(new Date()) + "||"+ this.getName());
        try {
            Thread.sleep(sleeps[random.nextInt(3)]);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("scheduleAtFixedRate 执行花费时间=" + (end - start));
        System.out.println("scheduleAtFixedRate 执行完成时间:" + DateFormat.getTimeInstance().format(new Date()));
        System.out.println("======================================");
    }
}

测试结论如下:

  • 任务执行时间 < 线程池间隔 —— 正常按间隔执行
  • 任务执行时间 > 线程池间隔 —— 按任务间隔执行,也就是当次任务执行后才会执行下一次任务,会打破间隔。
2)scheduleWithFixedDelay ( 固定延期安排

创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。

如果任务的任一执行遇到异常,就会取消后续执行。否则,只能通过执行程序的取消或终止方法来终止该任务。

测试:同样使用上面的例子,测试结果如下:

不难发现,每次任务结束后会固定停止delay秒后再开始下一次任务。和任务的执行时间无关。

06 阿里规范为什么禁止使用使用 Executors 去创建线程池

Executors 返回的线程池对象的弊端前面也已经做了分析,如下:

1) FixedThreadPool 和 SingleThreadPool:

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2) CachedThreadPool:

允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM应该是CPU占用过高。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xcong_Zhu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值