第一:每次new Thread 新建对象,性能差
第二:线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM
第三:缺少更多的功能,如更多执行、定期执行、线程中断。
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序,都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗。重用存在的线程,减少对象创建、消亡的开销,性能佳。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用、
running状态:能接受新提交的任务,并且能处理阻塞队列中的任务。
shutdown状态:当一个线程池实例处于关闭状态的时候,不能在接收新提交的任务,但是可以继续处理阻塞队列中已经保存的任务。在线程池处理running状态时,它调用shutdown方法,会使线程池进入到该状态。
stop状态:不能接收新的任务,也不处理队列中的任务。它会中断正在处理中的线程,在线程池处于running或shutdown状态时,如果调用shutdownNow的时候会使线程池进入到该状态
tidying状态:如果所用的任务都终止了,有效线程数为0,线程池会进入到该状态。之后调用terminated()方法会进入terminated状态。
terminated状态
根目录是Executor在JUC包下(java.util.concurrent),结构如下(这里说的是在JUC包下的子类):
|- Executor :负责线程的使用与调用的根接口
|--** ExecutorService 子接口:线程池的主要接口
|-- AbstractExecutorService 提供 ExecutorService 执行方法的默认实现
|-- DelegatedExecutorService
|-- ForkJoinPool
|-- ThreadPoolExecutor 线程池的实现类
|-- ScheduledExecutorService 子接口:负责线程的调度
|-- ScheduledThreadPoolExecutor:继承了 ThreadPoolExecutor,实现了ScheduledExecutorService
实际上最根本用的是ExecutorService,又因为ExecutorService是接口,接口不能创建对象,所以根本用的就是创建ThreadPoolExecutor的实例或ScheduledThreadPoolExecutor的实例。
但是基本上都是使用Executors工具类。最常见的有如下四个。但是这四个类都有可能OOM,一般情况下都需要根据自己需求,自定义线程池。
1、newFixedThreadPool() : 创建固定大小的线程池,返回类型是ExecutorService。
2、newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。返回类型是ExecutorService。
3、newSingleThreadExecutor() : 创建单个线程池,线程池中只有一个线程。返回类型是ExecutorService。
4、newScheduledThreadPool() : 创建固定大小的线程池,可以延迟或定时的执行任务。返回类型ScheduledExecutorService。
在介绍线程池原理的时候首先看一下这四个工具类源码。
其实这四个工具类都是实例化ThreadPoolExecutor这个类。进入ThreadPoolExecutor里查看这个构造函数。ThreadPoolExecutor构造函数有4种。这里介绍参数最多的。
public ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//表示线程没有任务执行时最多保持多久时间会终止。
TimeUnit unit, //参数keepAliveTime的时间单位,有7种取值
BlockingQueue workQueue, //阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响
ThreadFactory threadFactory, //线程工厂,用来创建线程
RejectedExecutionHandler handler //当拒绝处理任务时的策略
)
拒绝策略有四种
1、AbortPolicy 默认 处理程序遭到拒绝将抛出运行时 RejectedExecutionException
2、CallerRunsPolicy 用调用者所在的线程调用任务
3、DiscardOldestPolicy 如果执行程序没有关闭,阻塞队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
4、DiscardPolicy 不能执行的任务将被删除,只不过不抛出异常。
如果运行的线程数少于corePoolSize的时候,直接创建新线程处理任务。
如果运行的线程数大于等于corePoolSize,小于maximumPoolSize的时候,只有当workQueue满的时候才创建新的线程去处理任务。
如果设置的corePoolSize和maximumPoolSize相同的话,那么创建线程池大小是固定的。这时候如果有请求,workQueue还没满的时候,就把请求放入workQueue中,等待有空闲的线程从这里面提取。
为什么线程池要用阻塞队列而不用非阻塞队列?
因为线程执行需要时间,当队列满的情况下,遇到新的任务添加不进去,会出现丢任务的情况。用阻塞队列的话,可以阻
塞添加,等线程执行完,有空余线程,会执行阻塞队列里的任务,这样新的任务可以添加进去。
接下来具体介绍运行原理,以及核心线程数与最大线程数的关系。
提交一个任务到线程池中,线程池的处理流程有三步:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断阻塞队列是否已满,如果阻塞队列没有满,则将新提交的任务存储在这个阻塞队列里。如果阻塞队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务(也是放在线程池里)。如果已经满了,则交给饱和策略来处理这个任务。
如果上图不理解的话,可以结合代码在想着可能会更好理解。根据四个工具类可以得出想要自定义线程池就实例化ThreadPoolExecutor即可,就是根据需要实例化ThreadPoolExecutor,如下图,自定义一个核心线程数为1,最大线程数为2,阻塞队列为ArrayBlockingQueue。
public class Test {
public static void main(String[] args) {
// 核心线程数是1,最大线程数是2,阻塞队列是采用ArrayBlockingQueue(有边界的阻塞队列)
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
//当执行第一个任务时,直接从线程池中取出线程执行
threadPoolExecutor .execute(new TaskThread("任务1"));
//当执行第二个的时候,因为大于核心线程数,所以放在阻塞队列中
threadPoolExecutor .execute(new TaskThread("任务2"));
//当执行第三个的时候,因为大于核心线程数,所以放在阻塞队列中
threadPoolExecutor .execute(new TaskThread("任务3"));
//当执行第四个的时候,因为大于核心线程数,所以放在阻塞队列中
threadPoolExecutor .execute(new TaskThread("任务4"));
//当执行第五个的时候,因为大于核心线程数,且阻塞队列已经被占满,这个时候最大线程数还有空闲线程,所以新建一个线程执行该任务
threadPoolExecutor .execute(new TaskThread("任务5"));
//当放入第六个线程的时候,因为2个最大线程数和3个阻塞队列全被占用,所以会报错
threadPoolExecutor .execute(new TaskThread("任务6"));
//关闭线程池
threadPoolExecutor .shutdown();
}
}
class TaskThread implements Runnable {
private String taskName;
public TaskThred(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+taskName);
}
}
1、CPU密集型:花费了绝大多数时间在计算上
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务,就需要尽量压榨CPU,参考值可以设为CPU+1
2、I/O密集型: 花费了大多是时间在等待I/O上。
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型任务,参考值可以设置为2*CPU。