目录
前言
线程池的好处
使用Executors 创建常见的线程池
工厂模式:
往线程池当中添加任务
常见线程类
编辑 线程池的参数介绍
线程池的工作流程
补充
前言
如果我们需要频繁的创建销毁线程,此时创建销毁线程的成本,不能忽视了
因此就可以使用线程池.提前创建好一波线程,后续需要使用线程,就直接从“池子”里拿一个即可~
当线程不再使用,就放回池子里~~
本来,是需要创建线程/销毁线程,
现在,是从池子里获取到现成的线程,并且把线程归还到池子中这样比从系统这里创建线程更快更高效
线程池的好处
降低资源的消耗
通过重复利用已经创建的线程降低线程创建,销毁造成的开销。
我们知道,传统的操作当中,通过调用thread.start()来创建线程,这一个步骤,实际上是调用了操作系统提供的api,来创建PCB,这一个过程涉及操作系统内部的系统调用。
当线程执行完自己的任务之后,重新销毁线程。这个销毁的过程,也是交给操作系统完成的。
而如果使用了线程池,可以有效减少不断创建、销毁线程带来的损耗。
因为线程池创建之后,每次执行任务,都是从线程池当中获取线程,来执行任务,不用每次都通过调用api来获取。
提高响应速度
当任务需要执行的时候,任务可以不需要等待到线程创建,就可以立即执行任务。
提高线程的可管理能力
线程属于稀缺资源,如果无限制地创建线程,不仅仅会消耗系统资源,还会降低系统的稳定性。
如何理解所谓的"稳定性"呢?
相比于把线程的获取交给操作系统内核来完成,通过线程池直接获取线程,可以提高程序的"可控性"。
因为,如果直接创建线程,程序执行的时候,无法预知操作系统的内核当中究竟背负了多少的任务。操作系统内核也许背负了成千上万的任务......那轮到我需要执行的"创建线程"这个任务的时候,究竟响应速度怎样,能否响应都是问题......
这样,也就无法预知通过操作系统获取线程的真实情况,也就提高了管理线程的难度
如果是从系统这里创建线程,需要调用系统api,进一步的由操作系统内核,完成
线程的创建过程(内核是给所有的进程提供服务的)不可控的!!
如果是从线程池这里获取线程,上述的内核中进行的操作,都提前做好了,现在是可控的!!
取线程的过程,纯粹的用户代码完成(纯用户态)可控的!!
使用Executors 创建常见的线程池
此处,4指定的是线程池当中的核心最大线程数量,后面会提及。
线程池创建线程的方式,是通过"工厂模式"来实现的,是通过一个工厂来实现的。
这里创建线程对象,并没有直接new N个线程对象,而是通过一个特定的工厂(ThreadFactory)来生产线程,后面会介绍到。
我们Java有方法重载的机制,单当出现下面这种情况时,就不行了。
接下来,就可以用工场来解决这件事了。
工厂模式:
可以简单地理解为:使用普通方法,代替构造方法来创建对象。
如果想要获取对象,只能通过这个"普通方法"来获取对象。
并且,可以通过这个"工厂"来获取多个对象。
往线程池当中添加任务
在①当中,已经创建好10个线程的任务了,那么,线程的任务,如果执行呢?
此时,就需要使用submit()方法来把任务放到线程池当中,如下代码:
上图代码的含义是,往一个线程池当中初始化4个线程,然后再往这个线程池当中提交10个任务,由4个线程来"平均"分配一下这10个任务。
但是,不一定是每个线程都可以平均执行100个任务。尽管线程调度有记帐系统来尽可能让每个参与调度的线程执行的时间比较平均,但是也不可以百分百确保每个线程执行获取的任务数量都一模一样。
常见线程类
常见的
线程池的参数介绍
除了上述这些线程池之外,标准库还提供了一个接口更丰富的线程池类
ThreadPoolExecutor
看原码,
corePoolSize:核心线程数,线程池启动时就会创建的线程数量。即使核心线程是空闲的,也不会被回收,除非
调用了allowsCoreThreadTimeOut方法为true
executorService.allowCoreThreadTimeOut(true);
maximumPoolSize:最大线程数,线程池中最大的线程数量
keepAliveTime:线程超时时间,看源码可知,该参数的意义是线程从工作队列中取出任务的超时时间。其中,timed是指是否设置allowCoreThreadTimeOut为true,如果没有设置,则判断当前线程数是否大于核心线程数。timeout指的就是线程从队列中取任务是否超时。当一个线程从工作队列中取任务时,满足以下条件:(当前线程数大于最大线程数 或 (timed为true且该线程获取任务超时))&(当前线程数大于1 或 工作队列为空(说明不是繁忙时候)),这个线程就会被回收。
unit:超时时间的单位
workQueue:阻塞队列
threadFactory:线程工厂,要实现ThreadFactory接口,线程池创建线程时会调用ThreadFactory的newThread方法创建线程。
RejectedExecutionHandler:饱和策略,详情见下面的说明
如果线程池中的线程数量大于等于最大线程数,且工作队列已满,新提交的任务就会被拒绝,此时就要使用饱和策略。默认饱和策略是AbortPolicy,该策略会抛出rejectedExecution,调用者可以捕获该异常,编写处理代码。除了默认的饱和策略,还有其他几种饱和策略:
>DiscardPolicy:抛弃策略,会悄悄的抛弃没法加入到队列的任务
>DiscardOldestPolicy:抛弃最旧策略,会悄悄的抛弃下一个要执行的任务。如果使用的是优先队列,会抛弃优先级最高的任务,最好不要使用。
>CallerRunsPolicy:调用者运行策略,即不会抛出异常,也不会放弃执行。任务会被调用了execute方法的线程调用。比如,在main方法中开启线程池,如果线程池使用该策略,那么饱和的任务会被主线程调用。
testThreadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
线程池的工作流程
大致流程描述:
执行任务,核心线程数未达到corePoolSize该值,就创建线程。否则塞入任务队列。
任务队列满了,就创建线程,但是总线程数不能超过该值maximumPoolSize。
如果任务队列满了,线程数达到maximumPoolSize值,则执行失败策略。
工作线程则不停的轮询去队列中poll任务,如果poll为空,则工作线程执行结束(回收线程)。
如果工作线程数<=核心线程数corePoolSize,则使用take从队列中获取任务(核心线程一直await)。
任务队列缓冲
核心方法:boolean offer(E) 往队列中添加一个元素。如果队列已满,添加失败,返回fasle。
E poll(timeout, unit) 获取并移除队列头节点。如果队列为空,则等待${timeout}时间,如果还是没有数据,则返回null。
E take() 获取并移除队列头节点。如果队列为空,则一直等待。
补充
1、当设置核心线程数为0的时候,会创建一个非核心线程进行执行
2、当设置核心线程数不为0的时候,如果核心线程数在执行,会有一个非核心线程数从队列中取对象执行线程
3、核心线程数执行的是队列的take,非核心线程数执行队列的offer和poll
4、核心线程数不为0且队列为SynchronousQueue时,就成了单线程运行了