多线程是Java开发中的一个重要的领域,诚如我们所知道的,Java共有四种多线程的实现方式,第一种是集成Thread类,第二种是实现Runable接口(无返回值、不抛异常),第三种是实现Callable接口(有返回值、抛异常),第四种是通过线程池实现,本文主要介绍通过线程池的方式实现多线程。
一、为什么要使用线程池、优势是什么?
线程池的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程的数量超出了最大线程的数量则超出数量的线程排队等候,等待其他线程执行完毕后,再从队列中取出任务执行。
主要特点:线程复用、控制最大并发量、管理线程。
第一:降低资源消耗。通过重复利用已经创建的线程来减少和降低线程创建和销毁带来的消耗;
第二:提高响应速度。当任务到达时,不需要等待线程创建就能立即执行;
第三:提高线程的可管理性。线程时稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
二、线程池的三种常用方式
(1)架构
Java中的线程池主要是通过Executor框架实现,该框架中主要用到了Executor、Executors、ExecutorService、ThreadPoolExecutor这几个类。
线程池的底层实际上就是通过ThreadPoolExecutor实现。
(2)主要种类
实际开发中接触的比较多的主要是以下三类:
1)Executors.newFixedThreaPool(int):执行长期的任务性能较好
创建的是一个定长的线程池,可以较好的控制并发数,其他的线程将在队列中等待;
corePoolSize和maximumPoolSize相等,等待队列底层是通过LinkedBlockingQueue阻塞队列实现。
2)Executors.newSingleThreadExecutor
单线程的线程池,用一个工作线程来执行所有的任务,可以保证所有的任务按顺序执行;
corePoolSize和maximumPoolSize都是1,等待队列底层是阻塞队列LinkedBlockingQueue。
3)Executors.newCachedThreadPool
可缓存线程,工作线程根据实际处理需要灵活处理,若线程池数量超过处理需要,可灵活回收空闲线程,若无空闲线程则创建新线程执行任务;
将corePoolSize设置为0,maximumPoolSize设置为Integer.Max_value,等待队列底层通过SynchronousQueue阻塞队列实现,其处理流程的实质就是来一个任务就创建一个线程,线程空闲60S就自动销毁。
(3)线程池的七大参数
1)corePoolSize:核心线程数
在线程池创建之后,当请求任务来了之后,就会安排线程池中的线程去执行任务,核心线程可以近似理解为今日当值线程;
当池中正在执行的线程数大于或等于核心线程数时,新加入的任务加入到缓存队列中等待。
2)maximumPoolSize:最大线程数
线程池能容纳的最大线程数,设置的值必须大于等于1。可以近似理解为某银行网点固定的所有的服务柜台数。
3)keepaliveTime:线程空闲时间
多余空闲线程的存活时间,当前线程池数量超过corePoolSize时,当空闲时间达到keepaliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
4)timeunit:线程空闲时间单位
5)blockingQueue:等待(阻塞)队列。被提交但未执行的任务线程
6)threadFactory:生成线程池中的工作线程的线程工厂, 用于创建线程,一般默认的即可。类似于每个银行的标配,大堂经理、银行logo等。
7)hander:拒绝策略
当等待(阻塞)队列已满并且工作线程数大于或等于线程池最大线程数时会执行某种拒绝策略。
(4)线程池底层实现原理
1)线程池创建成功,等待提交过来的任务请求;
2)当调用了execute()方法后,线程池会做一下判断:
①当前正在运行的线程数量小于corePoolSize时,直接创建新线程执行任务;
②当前正在运行的线程数量大于或等于corePoolSize且等待(阻塞)队列未满时,将任务放入等待队列;
③当前等待(阻塞)队列已满且正在运行的线程数小于maximumPoolSize时,立即创建新的非核心线程执行新加入的任务;
④当前等待(阻塞)队列已满且正在运行的线程数大于或等于maximumPoolSize时,线程池会启动饱和拒绝策略来执行。
3)当一个线程处理完毕后,会从等待队列中取出下一个任务进行执行;
4)当一个线程长期处于无事可做的状态且达到一定的时间(keepaliveTime)时,如果当前运行的线程数量大于corePoolSize,那么会将这些空闲且达到规定空闲时间的线程销毁。由此我们也可以看出,当线程池中的所有任务执行完毕后线程池会收缩到corePoolSize大小。
(5)线程池的四种拒绝策略
1)何时触发拒绝策略
等待(阻塞)队列满了,并且线程池达到了max线程数,此时触发拒绝策略。
2)拒绝策略种类
(1)AbortPolicy(默认):直接抛出RejectedExecutionException,程序中断运行
(2)CallerRunsPolicy:调用者运行模式,不报错也不中止运行,而是将任务退回到调用线程
(3)DiscardOldestPolicy:丢弃等待队列中等待时间最长的任务,再把当前任务放入队列中再尝试提交任务
(4)DiscardPolicy:直接丢弃任务,不做任何处理
三、如何实现最优配置线程池
需根据实际的环境进行对应的配置,主要分为两种环境类型的配置:
前提:java获取CPU核数方法:
Runtime.getRuntime().availableProcessors()
1)CPU密集型
CPU密集型指的是任务需要大量的运算,没有阻塞,CPU一直在高速的运行。(CPU密集任务只有在真正的多核CPU上才能通过多线程实现加速,单核CPU莫得搞)。
线程池配置规则:
CPU核数 + 1个线程的线程池
2)IO密集型
IO密集型指的是任务需要大量的IO,即大量的阻塞。单线程中运行IO密集型的任务会导致大部分CPU的运算能力都浪费在IO阻塞等待上。所以IO密集型任务中使用多线程能够大大的加速程序运行,配置规则如下:
CPU核数 * 2
四、多线程死锁及定位分析解决
1、是什么
多线程死锁指的是两个或两个以上的线程在任务执行过程中相互争夺资源而导致的一种互相等待的现象,若无外力干涉他们都将无法继续向下推进。多线程死锁多发生在系统资源不足的情况下。
2、产生原因
1)系统资源不足
2)进程运行推进的顺序不合适
3)资源分配不当
3、解决方法
1)执行jps -l命令,找到死锁的进程号
2)执行jstack 进程号查看原因