作者主页:paper jie_博客
本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。
本文于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造,将MySQL基础知识一网打尽,希望可以帮到读者们哦。
其他专栏:《MySQL》《C语言》《javaSE》《数据结构》等
内容分享:本期将会分享线程池知识.
目录
引入
什么是线程池
为什么使用线程池会更高效
Java标准库中的线程池
ThreadPoolExecutor
Executors
创建线程池时,怎么设定线程数合适?
实现一个自定义的线程池
具体代码
引入
在开始,为了解决并发编程的问题,我们引入了进程. 但随着不断发展我们发现进程的创建销毁的开销会比较大,因此我们就又引入了进程.但是随着线程创建的频率提高,这样的开销又逐渐大起来了.这个时候我们就有了两种解决方法.第一种就是引入我们的纤程/携程. 纤程的本质上就是在用户态代码中调度和控制,这样就可以不让内核态来调度.这样就节省了调度的开销.纤程就是基于线程封装出来了,一般都是多个纤程对应一个线程.
第二个方法就是我们本文需要讲的线程池~
什么是线程池
在我学变成代码中,我们会经常遇到一些带有池的名词,比如: 常量池,数据库连接池,线程池等. 池的作用就是提前将对象创建好,在需要使用的时候就去池子里面拿,用完了也不要立刻销毁,而是再放回池里等待下次使用.这样就可以很好的提高效率,因为节省了创建对象和销毁对象的开销.
而线程池也是这样,它的本质也就是提前将线程在线程池中创建好,使用完后也不会立刻销毁,会返回线程池等待下一次的使用.这样也就是节省了创建和调度的开销.
为什么使用线程池会更高效
因为从线程池中拿线程是在用户态代码中调度执行的,是可控的.而直接创建线程是需要在内核态中创建,这时不可控的.因为你也不知道什么时候CPU才会给创建线程.
举个栗子:
这就像你去银行办理银行卡,它可能需要你打印身份证复印件.这时你有两个选择: 1. 直接拿着身份证去大厅的自助打印机打印. 2.把身份证交给工作人员,由她帮你打印. 要是你自己打印就是直接去中途不会干其他的事情,但是交给工作人员保不准她可能没有第一时间给你打印,而是先去干其他的事情. 这里办公区就属于内核态,大厅就属于用户态.
Java标准库中的线程池
ThreadPoolExecutor
Java标准库中的线程池是ThreadPoolExecutor. 它有多个参数,我们需要去了解一下.我们可以去Java的官方文档里面找 ThreadPoolExecutor (Java 平台 SE 8) (oracle.com) 我们需要找到concurrent这个包.这个包里就有ThreadPoolExecutor这个类. 我们可以找到它的构造方法.
这里我们理解第4个即可,其他几个的参数第4个都包含.
int corePoolSize: 核心线程数.可以理解为公司里的正式员工.
int maximumPoolSize: 最大线程数.可以理解为公司里的临时工,公司不忙了就可以开除的那种.
long keepAliveTime: 存活时间,就是除去核心线程的其他线程的存活时间. 可以理解为当零时工闲下来多久会被开除.
TimeUnit unit: 存活时间的单位.
BlockingQueue<Runnable> workQueue: 阻塞队列,这个队列里面存放的就是线程需要执行的任务,线程会到里面去取任务.
ThreadFactory threadFactory: 线程工厂,线程池就是通过这个工厂类来创建线程. 本质上就是将创建线程对象的操作封装起来且再设置一些线程的属性.
RejectedExecutionHandler handler: 拒绝策略. 当阻塞队列满了之后,再添加新的任务进来,这个拒绝策略就会出来处理. 它提供了4个方法:
1. 直接抛出异常,新的任务和旧的任务都不执行了.
2. 新的任务由这个添加新任务的线程来执行.
3. 丢弃最旧的任务,再将这个新的任务添加进阻塞队列.
4. 丢弃这个需要添加进来的新任务,继续照常执行.
Executors
因为这个类的参数比较多,用起来比较复杂.Java就又用一个类将它封装起来了,变成了一个比较简单的线程类Executors.它也是一个工厂类. 也是通过这个类创建好不同的线程池对象,在它的内部就已经创建好了线程池对象且设置了一些它的参数.
它有好几种创建线程池的方式:
newFixedThreadPool | 创建固定线程数的线程池 |
newCachedThreadPool | 创建线程数可以动态增长的线程池 |
newSingleThreadExecutor | 创建只含有单个线程的线程池 |
newScheduledThreadPool | 创建线程可以延时执行任务的线程池,类似于定时器 |
这里两个类我们可以看情况来使用.
创建线程池时,怎么设定线程数合适?
这里用一句话来概括就是具体问题具体分析.
我们线程执行任务分为两种: 一种为CPU密集型,一种为IO密集型.CPU密集型的线程就是使用CPU的时间比较长.而IO密集型的线程就是使用CPU时间少,大多数时间都是在IO等待中. 这里极端一点,都是CPU密集型的话线程池的线程数不能超过逻辑核心数,而都是IO密集型的话线程数就可以远远超过逻辑核心数了.
但是在我们实际开发中,一般都是CPU密集型一部分,IO密集型一部分.这种情况下我们就需要具体问题具体分析了.最好的办法就是进行性能测试,给线程池的线程数进行多组不同数目的测试,观察他们的系统资源开销和时间开销,取其中最好的即可.
实现一个自定义的线程池
这里我们实现一个简单的固定线程数的线程池.
我们需要:
1. 一个存放任务的阻塞队列
2. 一个核心方法来添加任务.
3. 用构造方法来指定线程数,创建好线程.
具体代码
class MyThreadPoolExecutor3 {//1. 存放任务的阻塞队列private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);//2. 构造方法public MyThreadPoolExecutor3(int capacity) {for(int i = 0; i <capacity; i++) {Thread t = new Thread(() -> {while(true) {Runnable runnable = null;try {runnable = queue.take();} catch (InterruptedException e) {throw new RuntimeException(e);}runnable.run();}});t.start();}}//核心方法 submit 添加方法public void submit(Runnable runnable) throws InterruptedException {queue.put(runnable);}
}
public class ThreadDemo3 {public static void main(String[] args) throws InterruptedException {MyThreadPoolExecutor3 myThreadPoolExecutor3 = new MyThreadPoolExecutor3(4);for (int i = 0; i < 1000; i++) {int n = i;myThreadPoolExecutor3.submit(() -> {System.out.println(n + " " + Thread.currentThread().getName() + " hello");});}}}