线程池的基础与操作
一、概念
线程池有四类:
1、newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。(线程最大并发数不可控制)
2、newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
4、newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
其中ScheduledThreadPool有个scheduel方法,向其中传入runnable接口(表示要执行的任务)、delay时间、时间单元timeunit
使用ThreadPoolExecutor类:包含了五个参数int
corePoolSize(线程池的基本大小),
int
maximumPoolSize(最大线程池容量),
long
keepAliveTime(存活时间),TimeUnit unit(时间单位),
BlockingQueue<Runnable> workQueue(任务队列)
corePoolSize:当线程池刚刚创建的时候,线程池中没用线程,当有任务创建且当前线程数小于corePoolSize时才会去创建新的线程。但是当调用prestartAllCoreThreads或prestartCoreThread方法时候才会在创建时就创建corePoolSize
个或一个线程。当创建的线程数等于corePoolSize时,就不再创建新线程,而是放到缓存队列中,当缓存队列(缓存队列的大小通过BlockingQueue来设置)满了的时候,才开始创建新的线程。
maximumPoolSize:线程创建的最大数,当线程数未达到该值,并且任务队列已满,就可以新创建线程。
当线程达到maximumPoolSize,并且队列也已经满了,如果再有新任务就会采取饱和策略,默认采取AbortPolicy,用来抛出异常,表示不能再处理新的任务。
有四种策略:
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
- 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
keepAliveTime:线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率,避免重复的创建销毁线程。
TimeUnit:时间单位:线程keepAliveTime的时间单位
BlockingQueue:任务缓存队列,用于保存线程数达到corePoolSize之后的阻塞队列。可以选择以下几种阻塞队列:
1.ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2.LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
3.SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4.PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
流程图:
二、操作
1、线程池创建:
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 200, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));
2、使用线程池执行任务
//execute中的参数是一个runnable接口实例 threadsPool.execute(new Runnable() { public void run(){ } } //也可以使用一个实例先实现runnable Class task implements Runnable(){ public void run(){ } } threadsPool.execute(task);
3、简单的线程例子
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Test { public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 200, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5)); for(int i=0;i<15;i++){ MyTask myTask = new MyTask(i); executor.execute(myTask); System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+ executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount()); } executor.shutdown(); } } class MyTask implements Runnable { private int taskNum; public MyTask(int num) { this.taskNum = num; } @Override public void run() { System.out.println("正在执行task "+taskNum); try { Thread.currentThread().sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("task "+taskNum+"执行完毕"); } }
4、线程执行任务有两个方法:submit和execute
不同点:
1、submit接受的参数可以是callable接口实例,runnable接口实例;而execute接受的参数只有runnable接口实例
2、对待异常的处理不同,execute和正常的一样抛出异常,而submit如果不是用get方法的时候就会吃掉异常,返回的future对象使用get方法后才会打印简略异常。
3、submit的返回的future对象使用get方法由于需要等待接收任务的返回值所以会阻塞线程池,同步阻塞执行任务,任务执行成功,就返回一个null。
三、线程池的合理配置考虑
根据任务的不同进行分门别类的处理:
1、任务的性质不同:io密集型任务、cpu运算密集型任务、混合型任务
2、优先级不同:高中低
3、需要时间不同的任务:短中长
4、是否有依赖
1、性质不同: 如果是cpu运算密集型的任务的话,由于运算快,线程的利用率更高,所以尽可能少的设置最大线程数,cpu+1个为好
如果是io存储密集型任务的话,由于存储时io较为耗时,线程利用率更低,所以设置2*task个线程为好。
2、时间不同和优先级不同以及有否依赖都可以通过使用优先级队列来解决
时间短的优先级大些先完成,优先级高的也将其设置为高优先级,依赖底层的也设置高优先级
风险:建议使用有界队列,如果使用无界队列,一旦任务耗时严重,线程达到了线程corepoolsize,就会往任务队列里存放,如果一直存放的话会造成内存溢出oom异常。