基本概念
线程池
在JAVA中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便。但是就会有一个问题,如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?这样Java线程池出现了。
new Thread的弊端:
- a. 每次new Thread新建对象性能差。
- b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
- c. 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,线程池的好处:
- a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
- b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
- c. 提供定时执行、定期执行、单线程、并发数控制等功能
ThreadPoolExecutor方法:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
序号 | 名称 | 类型 | 含义 |
---|---|---|---|
1 | corePoolSize | int | 核心线程池大小 |
2 | maximumPoolSize | int | 最大线程池大小 |
3 | keepAliveTime | long | 线程最大空闲时间 |
4 | unit | TimeUnit | 时间单位 |
5 | workQueue | BlockingQueue | 线程等待队列 |
6 | threadFactory | ThreadFactory | 线程创建工厂 |
7 | handler | RejectedExecutionHandler | 拒绝策略 |
-
corePoolSize - 核心池的大小
这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。 -
maximumPoolSize - 线程池最大线程数
这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程。 -
keepAliveTime - 表示线程没有任务执行时最多保持多久时间会终止。
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。 -
unit - 参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; //天 TimeUnit.HOURS; //小时 TimeUnit.MINUTES; //分钟 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //纳秒
-
workQueue - 一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue; ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
一般如果线程池任务队列采用LinkedBlockingQueue队列的话,那么不会拒绝任何任务(因为队列大小没有限制),这种情况下,ThreadPoolExecutor最多仅会按照最小线程数来创建线程,也就是说线程池大小被忽略了。
如果线程池任务队列采用 ArrayBlockingQueue队列的话,那么ThreadPoolExecutor将会采取一个非常负责的算法,比如假定线程池的最小线程数为4,最大为8所用的ArrayBlockingQueue最大为10。随着任务到达并被放到队列中,线程池中最多运行4个线程(即最小线程数)。
即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也只会利用4个线程。如果队列已满,而又有新任务进来,此时才会启动一个新线程,这里不会因为队列已满而拒接该任务,相反会启动一个新线程。新线程会运行队列中的
第一个任务,为新来的任务腾出空间。这个算法背后的理念是:该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀。如果挤压的请求变得非常多,这时该池就会尝试运行更多的线程来清理;
这时第二个节流阀—最大线程数就起作用了。
-
threadFactory - 执行程序创建新线程时使用的工厂,主要用来创建线程。
-
handler - 表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
Java通过Executors提供四种线程池,分别为:
这几中线程池底层都是通过ThreadPoolExecutor来实现的:
-
1、newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 -
2、newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 -
3、newScheduledThreadPool
创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。 -
4、newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
四种线程池的参数对比
种类 | corePoolSize | maximumPoolSize | keepAliveTime | unit | workQueue | ------------- 适用场景 ----------------------- |
---|---|---|---|---|---|---|
newCachedThreadPool() | 0 | Integer.MAX_VALUE | 60L | TimeUnit.SECONDS | new SynchronousQueue()【同步队列】 | 执行很多短期异步的小程序或者负载较轻的服务器【Netty的NIO接受请求时】 |
newFixedThreadPool(8) | nThreads(传入参数) | nThreads(传入参数) | 0L | TimeUnit.MILLISECONDS | new LinkedBlockingQueue()【无界阻塞队列】 | 执行长期的任务,性能好很多【可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。】 |
newSingleThreadExecutor() | 1 | 1 | 0L | TimeUnit.MILLISECONDS | new LinkedBlockingQueue())【无界阻塞队列】 | 一个任务一个任务执行的场景 |
newScheduledThreadPool(3) | 传递来的参数 | Integer.MAX_VALUE | 0 | TimeUnit.NANOSECONDS | new DelayedWorkQueue()【 一个按超时时间升序排序的队列】 | 周期性执行任务的场景 |
Executors 框架
Executor VS ExecutorService VS Executors
正如上面所说,这三者均是 Executor 框架中的一部分。Java 开发者很有必要学习和理解他们,以便更高效的使用 Java 提供的不同类型的线程池。总结一下这三者间的区别,以便大家更好的理解:
- Executor 和 ExecutorService 这两个接口主要的区别是:ExecutorService 接口继承了 Executor
接口,是 Executor 的子接口 - Executor 和 ExecutorService 第二个区别是:Executor 接口定义了
execute()方法用来接收一个Runnable接口的对象,而 ExecutorService 接口中的
submit()方法可以接受Runnable和Callable接口的对象。 - Executor 和 ExecutorService 接口第三个区别是 Executor 中的 execute() 方法不返回任何结果,而
ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。 - Executor 和 ExecutorService 接口第四个区别是除了允许客户端提交一个任务,ExecutorService
还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。可以通过 《Java Concurrency in
Practice》 一书了解更多关于关闭线程池和如何处理 pending 的任务的知识。 - Executors 类提供工厂方法用来创建不同类型的线程池。比如: newSingleThreadExecutor()
创建一个只有一个线程的线程池,newFixedThreadPool(int
numOfThreads)来创建固定线程数的线程池,newCachedThreadPool()可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。
例子
public class thread{
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> resultList = new ArrayList<Future<String>>();
//创建10个任务并执行
for (int i = 0; i < 10; i++) {
//使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中
Future<String> future = executorService.submit(new TaskWithResult(i));
//将任务执行结果存储到List中
resultList.add(future);
}
//遍历任务的结果
for (Future<String> fs : resultList) {
try {
while (!fs.isDone()) ;//Future返回如果没有完成,则一直循环等待,直到Future返回完成
System.out.println(fs.get()); //打印各个线程(任务)执行的结果
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
//启动一次顺序关闭,执行以前提交的任务,但不接受新任务
executorService.shutdown();
}
}
static class TaskWithResult implements Callable<String> {
private int id;
public TaskWithResult(int id) {
this.id = id;
}
/**
* 任务的具体过程,一旦任务传给ExecutorService的submit方法,
* 则该方法自动在一个线程上执行
*/
public String call() throws Exception {
System.out.println("call()方法被自动调用!!! " + Thread.currentThread().getName());
//该返回结果将被Future的get方法得到
return "call()方法被自动调用,任务返回的结果是:" + id + " " + Thread.currentThread().getName();
}
}
}