写点什么

产品经理问我:手动创建线程不香吗,为什么非要用线程池呢?

用户头像
Java鱼仔
关注
发布于: 2021 年 02 月 19 日

听说微信搜索《Java 鱼仔》会变更强哦!

本文收录于JavaStarter ,里面有我完整的 Java 系列文章,学习或面试都可以看看哦


每次写线程池的文章时,总会想起自己大三第一次面试就是挂在这上面,当时年少轻狂,连 SpringBoot 是什么都不知道就敢面阿里,真是初生牛犊不怕虎。


(一)什么是线程池

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位,我们的程序最终都是由线程进行运作。在 Java 中,创建和销毁线程的动作是很消耗资源的,因此就出现了所谓“池化资源”技术。


线程池是池化资源技术的一个应用,所谓线程池,顾名思义就是预先按某个规定创建若干个可执行线程放入一个容器中(线程池),需要使用的时候从线程池中去取,用完之后不销毁而是放回去,从而减少了线程创建和销毁的次数,达到节约资源的目的。


(二)为什么要使用线程池

2.1 降低资源消耗

前面已经讲到线程池的出现减少了线程创建和销毁的次数,每个线程都可以被重复利用,可执行多个任务。


2.2 提高系统的响应速度

每当有任务到来时,直接复用线程池中的线程,而不需要等待新线程的创建,这个动作可以带来响应速度的提升


2.3 防止过多的线程搞坏系统

可以根据系统的承受能力,调整线程池中的工作线程的数量,防止因为线程过多服务器变慢或死机。java 一个线程默认占用空间为 1M,可以想象一旦手动创建线程过多极有可能导致内存溢出。


(三)线程池主要参数

我们可以用 Executors 类来创建一些常用的线程池,但是像阿里是禁止直接通过 Executors 类直接创建线程池的,具体的原因稍后再谈。


在了解 Executors 类所提供的几个线程池前,我们首先来了解一下

ThreadPoolExecutor 的主要参数,ThreadPoolExecutor 是创建线程池的类,我们选取参数最多的构造方法来看一下:


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


|名称 | 类型 |含义 |

|--|--|--|

corePoolSize |int| 核心线程池的大小

maximumPoolSize| int |最大线程池大小

keepAliveTime |long |线程最大空闲时间

unit |TimeUnit| 时间单位

workQueue |BlockingQueue<Runnable> |线程等待队列

threadFactory| ThreadFactory | 线程创建工程

handler| RejectedExecutionHandler | 拒绝策略


3.1 corePoolSize

当向线程池提交一个任务时,如果线程池中已创建的线程数量小于 corePoolSIze,即便存在空闲线程,也会创建一个新线程来执行任务,直到创建的线程数大于或等于 corePoolSIze。

3.2 maximumPoolSize

线程池所允许的最大线程个数,当队列满了且已经创建的线程数小于 maximumPoolSize 时,会创建新的线程执行任务。

3.3 keepAliveTime

当线程中的线程数大于 corePoolSIze 时,如果线程空闲时间大于 keepAliveTime,该线程就会被销毁。

3.4 unit

keepAliveTime 的时间单位

3.5 workQueue

用于保存等待执行任务的队列

3.6 threadFactory

用于创建新线程。threadFactory 创建的线程也是采用 new Thread()方式,threadFactory 创建的线程名都具有统一的风格:pool-m-thread-n

3.7 handler

拒绝策略,当线程池和队列满了之后,再加入新线程后会执行此策略。

下面是四种线程池的拒绝策略:


AbortPolicy:中断任务并抛出异常


DiscardPolicy:中段任务但是不抛出异常


DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试提交新任务


CallerRunsPolicy:由调用线程处理该任务


(四)线程池执行流程

当我们了解了 ThreadPoolExecutor 的七个参数后,我们就可以很快的理解线程池的流程:



当提交任务后,首先判断当前线程数是否超过核心线程数,如果没超过则创建新线程执行任务,否则判断工作队列是否已满,如果未满则将任务添加到队列中,否则判断线程数是否超过最大线程数,如果未超过则创建线程执行任务,否则执行拒绝策略。


(五)Executors 提供的线程池

executors 提供了许多种线程池供用户使用,虽然很多公司禁止使用 executors 创建线程池,但是对于刚开始解除线程池的人来说,Executors 类所提供的线程池能很好的带你进入多线程的世界。

5.1 newSingleThreadExecutor


ExecutorService executorService = Executors.newSingleThreadExecutor();
复制代码


听名字就可以知道这是一个单线程的线程池,在这个线程池中只有一个线程在工作,相当于单线程执行所有任务,此线程可以保证所有任务的执行顺序按照提交顺序执行,看构造方法也可以看出,corePoolSize 和 maximumPoolSize 都是 1。


public static ExecutorService newSingleThreadExecutor() {    return new FinalizableDelegatedExecutorService        (new ThreadPoolExecutor(1, 1,                                0L, TimeUnit.MILLISECONDS,                                new LinkedBlockingQueue<Runnable>()));}
复制代码


5.2 newFixedThreadPool


ExecutorService executorService = Executors.newFixedThreadPool(2);
复制代码


固定长度的线程池,线程池的长度在创建时通过变量传入。下面是 newFixedThreadPool 的构造方法,corePoolSize 和 maximumPoolSize 都是传入的参数值


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

5.3 newCachedThreadPool


ExecutorService executorService = Executors.newCachedThreadPool();
复制代码


可缓存线程池,这个线程池设定 keepAliveTime 为 60 秒,并且对最大线程数量几乎不做控制。


public static ExecutorService newCachedThreadPool() {    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                  60L, TimeUnit.SECONDS,                                  new SynchronousQueue<Runnable>());}
复制代码


观察构造方法,corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制。设定 keepAliveTime 为 60 秒,线程空闲 60 秒后自动结束,因为该线程池创建无限制,不会有队列等待,所以使用 SynchronousQueue 同步队列。


5.4 newScheduledThreadPool

创建一个定时的线程池。此线程池支持定时以及周期性执行任务的需求。下面是 newScheduledThreadPool 的用法:


Thread thread1=new Thread(new Runnable() {    @Override    public void run() {        System.out.println(Thread.currentThread().getName()+"thread1");    }});Thread thread2=new Thread(new Runnable() {    @Override    public void run() {        System.out.println(Thread.currentThread().getName()+"thread2");    }});Thread thread3=new Thread(new Runnable() {    @Override    public void run() {        System.out.println(Thread.currentThread().getName()+"thread3");    }});ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();//在1000ms后执行thread1scheduledExecutorService.schedule(thread1,1000,TimeUnit.MILLISECONDS);//在1000ms后每隔1000ms执行一次thread2,如果任务执行时间比间隔时间长,则延迟执行scheduledExecutorService.scheduleAtFixedRate(thread2,1000,1000,TimeUnit.MILLISECONDS);//和第二种方式类似,但下一次任务开始的时间为:上一次任务结束时间(而不是开始时间) + delay时间scheduledExecutorService.scheduleWithFixedDelay(thread3,1000,1000,TimeUnit.MILLISECONDS);
复制代码


(六)为什么阿里巴巴禁止程序员用 Exectors 创建线程池

如果你的 idea 装了 Alibaba Java Codeing Guidelines 插件(推荐大家使用,有助于让你的代码更加规范),那么当你写了 Exectors 创建线程池后会看到提示:



并且阿里将这个用法定义为 Blocker,即不允许使用,而是让人们用 ThreadPoolExecutor 的方式创建线程池。原因是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的人更加明确线程池的运行规则,规避资源耗尽的风险。



Executors 返回的线程池对象的弊端如下:


1)FixedThreadPool 和 SingleThreadPool:


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

  

2)CachedThreadPool:


  允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

  

下面是 ThreadPoolExecutor 创建线程池的简单例子


int corePoolSize=5;int maximumPoolSize=10;long keepAliveTime=30;BlockingQueue<Runnable> blockingQueue=new ArrayBlockingQueue(2);RejectedExecutionHandler handler=new ThreadPoolExecutor.AbortPolicy();ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, blockingQueue, handler);threadPoolExecutor.execute(thread1);
复制代码


(七)在 SpringBoot 项目中使用线程池

SpringBoot 对线程池又做了一层封装,在 SpringBoot 中,可以通过 ThreadPoolTaskExecutor 类来创建线程池。需要注意两者的区别 ThreadPoolExecutor 时 JUC 包下的类,ThreadPoolTaskExecutor 是 springframework 包下的类。但原理都是一样的。


7.1 项目搭建

首先搭建一个 SpringBoot 项目,只需要引入 web 依赖即可


<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-web</artifactId></dependency>
复制代码


7.2 配置文件配置

线程池的参数尽量写到配置文件里,这样就能根据需要进行修改,在 application.properties 文件中配置:


myExecutor.corePoolSize=5myExecutor.maxPoolSize=10myExecutor.keepAliveSeconds=30myExecutor.allowCoreThreadTimeOut=falsemyExecutor.queueCapacity=20myExecutor.threadNamePrefix=myExecutor-
复制代码


7.3 编写一个自己的线程池

新建一个包叫 config,新建类 ExecutorConfig ,首先参数的值从配置文件中获取,接着用 ThreadPoolTaskExecutor 创建一个自己的线程池,注入到 Bean 容器中。

@Configuration@EnableAsyncpublic class ExecutorConfig {
@Value("${myExecutor.corePoolSize}") private int corePoolSize; @Value("${myExecutor.maxPoolSize}") private int maxPoolSize; @Value("${myExecutor.keepAliveSeconds}") private int keepAliveSeconds; @Value("${myExecutor.allowCoreThreadTimeOut}") private boolean allowCoreThreadTimeOut; @Value("${myExecutor.queueCapacity}") private int queueCapacity; @Value("${myExecutor.threadNamePrefix}") private String threadNamePrefix;
@Bean("myExecutor") public Executor myExecutor(){ ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor(); //核心线程数 executor.setCorePoolSize(corePoolSize); //最大线程数 executor.setMaxPoolSize(maxPoolSize); //线程空闲时间 executor.setKeepAliveSeconds(keepAliveSeconds); //是否保留核心线程数 executor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut); //队列长度 executor.setQueueCapacity(queueCapacity); //拒绝策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); //设置线程名称前缀 executor.setThreadNamePrefix(threadNamePrefix); executor.initialize(); return executor; }}
复制代码


这里需要注意的是有一个方法 setAllowCoreThreadTimeOut,当传入参数为 true 时,所有线程超时后都会被销毁,如果为 false,只有超过核心线程数并且超时时才会被销毁。


7.4 编写 service 并使用

首先写一个 service 接口:


public interface DoSomeThing {    /**     * 通过线程池异步执行耗时的任务     */    public void doSomeThing();}
复制代码


再编写实现类:


@Servicepublic class DoSomeThingImpl implements DoSomeThing {
@Override @Async("myExecutor") public void doSomeThing() { System.out.println(Thread.currentThread().getName()+"-in"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-out"); }}
复制代码


增加注解 @Async("myExecutor")后,这段方法就会异步由 myExecutor 线程池执行。


7.5 编写一个 controller

新建一个 IndexController,每次请求执行一次 doSomeThing()方法。


@RestControllerpublic class IndexController {
@Autowired private DoSomeThing doSomeThing;
@RequestMapping(value = "/index",method = RequestMethod.GET) public String index(){ doSomeThing.doSomeThing(); return "success"; }}
复制代码


7.6 测试

访问十次 http://localhost:8080/index,由于设置的核心线程数是 5,队列容量是 30,因此最多只会用到 5 个线程资源,结果如下:



(八)总结

线程池不算什么很难的技术,但是一定要掌握,面试也会经常问,工作中也会用到。要趁别人不注意的时候悄悄拔尖,然后惊艳所有人,我们下期再见。


发布于: 2021 年 02 月 19 日阅读数: 45
用户头像

Java鱼仔

关注

你会累是因为你在走上坡路 2020.12.26 加入

微信搜索《Java鱼仔》

评论

发布
暂无评论
产品经理问我:手动创建线程不香吗,为什么非要用线程池呢?