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

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

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


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


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


(一)什么是线程池


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


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


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


2.1 降低资源消耗


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


2.2 提高系统的响应速度


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


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


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


(三)线程池主要参数


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


在了解Executors类所提供的几个线程池前,我们首先来了解一下 ThreadPoolExecutor的主要参数,ThreadPoolExecutor是创建线程池的类,我们选取参数最多的构造方法来看一下:


publicThreadPoolExecutor(intcorePoolSize,
intmaximumPoolSize,
longkeepAliveTime,
TimeUnitunit,
BlockingQueue<Runnable>workQueue,
ThreadFactorythreadFactory,
RejectedExecutionHandlerhandler)


名称 类型

含义

corePoolSize

int

核心线程池的大小
maximumPoolSize
int
最大线程池大小
keepAliveTime

long

线程最大空闲时间

unit

TimeUnit

时间单位

workQueue

BlockingQueue

线程等待队列

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


ExecutorServiceexecutorService=Executors.newSingleThreadExecutor();

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


publicstaticExecutorServicenewSingleThreadExecutor() {
returnnewFinalizableDelegatedExecutorService        (newThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
newLinkedBlockingQueue<Runnable>()));
}

5.2 newFixedThreadPool


ExecutorServiceexecutorService=Executors.newFixedThreadPool(2);

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


publicstaticExecutorServicenewFixedThreadPool(intnThreads) {
returnnewThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
newLinkedBlockingQueue<Runnable>());
}

5.3 newCachedThreadPool


ExecutorServiceexecutorService=Executors.newCachedThreadPool();

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


publicstaticExecutorServicenewCachedThreadPool() {
returnnewThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
newSynchronousQueue<Runnable>());
}

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


5.4 newScheduledThreadPool


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


Threadthread1=newThread(newRunnable() {
@Overridepublicvoidrun() {
System.out.println(Thread.currentThread().getName()+"thread1");
    }
});
Threadthread2=newThread(newRunnable() {
@Overridepublicvoidrun() {
System.out.println(Thread.currentThread().getName()+"thread2");
    }
});
Threadthread3=newThread(newRunnable() {
@Overridepublicvoidrun() {
System.out.println(Thread.currentThread().getName()+"thread3");
    }
});
ScheduledExecutorServicescheduledExecutorService=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创建线程池的简单例子


intcorePoolSize=5;
intmaximumPoolSize=10;
longkeepAliveTime=30;
BlockingQueue<Runnable>blockingQueue=newArrayBlockingQueue(2);
RejectedExecutionHandlerhandler=newThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutorthreadPoolExecutor=newThreadPoolExecutor(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@EnableAsyncpublicclassExecutorConfig {
@Value("${myExecutor.corePoolSize}")
privateintcorePoolSize;
@Value("${myExecutor.maxPoolSize}")
privateintmaxPoolSize;
@Value("${myExecutor.keepAliveSeconds}")
privateintkeepAliveSeconds;
@Value("${myExecutor.allowCoreThreadTimeOut}")
privatebooleanallowCoreThreadTimeOut;
@Value("${myExecutor.queueCapacity}")
privateintqueueCapacity;
@Value("${myExecutor.threadNamePrefix}")
privateStringthreadNamePrefix;
@Bean("myExecutor")
publicExecutormyExecutor(){
ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();
//核心线程数executor.setCorePoolSize(corePoolSize);
//最大线程数executor.setMaxPoolSize(maxPoolSize);
//线程空闲时间executor.setKeepAliveSeconds(keepAliveSeconds);
//是否保留核心线程数executor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut);
//队列长度executor.setQueueCapacity(queueCapacity);
//拒绝策略executor.setRejectedExecutionHandler(newThreadPoolExecutor.AbortPolicy());
//设置线程名称前缀executor.setThreadNamePrefix(threadNamePrefix);
executor.initialize();
returnexecutor;
    }
}


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


7.4 编写service并使用


首先写一个service接口:


publicinterfaceDoSomeThing {
/*** 通过线程池异步执行耗时的任务*/publicvoiddoSomeThing();
}

再编写实现类:


@ServicepublicclassDoSomeThingImplimplementsDoSomeThing {
@Override@Async("myExecutor")
publicvoiddoSomeThing() {
System.out.println(Thread.currentThread().getName()+"-in");
try {
Thread.sleep(5000);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
System.out.println(Thread.currentThread().getName()+"-out");
    }
}

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


7.5 编写一个controller


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


@RestControllerpublicclassIndexController {
@AutowiredprivateDoSomeThingdoSomeThing;
@RequestMapping(value="/index",method=RequestMethod.GET)
publicStringindex(){
doSomeThing.doSomeThing();
return"success";
    }
}

7.6 测试


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


网络异常,图片无法展示
|


(八)总结


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



相关文章
|
2月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
63 1
|
2月前
|
存储 监控 安全
一天十道Java面试题----第三天(对线程安全的理解------>线程池中阻塞队列的作用)
这篇文章是Java面试第三天的笔记,讨论了线程安全、Thread与Runnable的区别、守护线程、ThreadLocal原理及内存泄漏问题、并发并行串行的概念、并发三大特性、线程池的使用原因和解释、线程池处理流程,以及线程池中阻塞队列的作用和设计考虑。
|
16天前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
5天前
|
Java
直接拿来用:进程&进程池&线程&线程池
直接拿来用:进程&进程池&线程&线程池
|
2月前
|
缓存 Java
异步&线程池 线程池的七大参数 初始化线程的4种方式 【上篇】
这篇文章详细介绍了Java中线程的四种初始化方式,包括继承Thread类、实现Runnable接口、实现Callable接口与FutureTask结合使用,以及使用线程池。同时,还深入探讨了线程池的七大参数及其作用,解释了线程池的运行流程,并列举了四种常见的线程池类型。最后,阐述了在开发中使用线程池的原因,如降低资源消耗、提高响应速度和增强线程的可管理性。
异步&线程池 线程池的七大参数 初始化线程的4种方式 【上篇】
|
1天前
|
Java
COMATE插件实现使用线程池高级并发模型简化多线程编程
本文介绍了COMATE插件的使用,该插件通过线程池实现高级并发模型,简化了多线程编程的过程,并提供了生成结果和代码参考。
|
27天前
|
监控 Java
线程池中线程异常后:销毁还是复用?技术深度剖析
在并发编程中,线程池作为一种高效利用系统资源的工具,被广泛用于处理大量并发任务。然而,当线程池中的线程在执行任务时遇到异常,如何妥善处理这些异常线程成为了一个值得深入探讨的话题。本文将围绕“线程池中线程异常后:销毁还是复用?”这一主题,分享一些实践经验和理论思考。
49 3
|
2月前
|
缓存 Java 调度
【Java 并发秘籍】线程池大作战:揭秘 JDK 中的线程池家族!
【8月更文挑战第24天】Java的并发库提供多种线程池以应对不同的多线程编程需求。本文通过实例介绍了四种主要线程池:固定大小线程池、可缓存线程池、单一线程线程池及定时任务线程池。固定大小线程池通过预设线程数管理任务队列;可缓存线程池能根据需要动态调整线程数量;单一线程线程池确保任务顺序执行;定时任务线程池支持周期性或延时任务调度。了解并正确选用这些线程池有助于提高程序效率和资源利用率。
40 2
|
2月前
|
缓存 监控 Java
Java性能优化:从单线程执行到线程池管理的进阶实践
在Java开发中,随着应用规模的不断扩大和用户量的持续增长,性能优化成为了一个不可忽视的重要课题。特别是在处理大量并发请求或执行耗时任务时,单线程执行模式往往难以满足需求,这时线程池的概念便应运而生。本文将从应用场景举例出发,探讨Java线程池的使用,并通过具体案例和核心代码展示其在实际问题解决中的强大作用。
|
2月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
63 6