为什么要使用线程池?(阿里巴巴开发手册推荐使用线程池)
在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多,更好的解决方案是合理地利用线程池,线程池的优势很明显,如下:
1.降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
2.提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
3.方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
4.更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。
一、线程池相关的类和接口
- Executor 接口
Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),如下:
public interface Executor {
void execute(Runnable command);
}
ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,返回 Future 对象,以及可跟踪一个或多个异步任务执行状况返回Future的方法;可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。
AbstractExecutorService抽象类实现的是ExecutorService接口。
ThreadPoolExecutor又是继承自AbstractExecutorService类。
- Executors工具类
Executors工具类是用来创建线程池的工厂类,内部返回的是ThreadPoolExecutor类的对象,简化了创建线程池的过程(推荐使用)。如果自带的几种创建方法无法满足要求的话,需要自己手动构建配置ThreadPoolExecutor对象。
二、ThreadPoolExecutor类
1.ThreadPoolExecutor类的构造函数
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
2.ThreadPoolExecutor的成员变量
(1)corePoolSize:核心池的大小,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务。
(2)maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程;
按照JDK文档的描述,设poolSize为当前线程池的线程量,则有如下关系:
(a)如果poolSize<corePoolSize,新增加一个线程处理新的任务。
(b)如果poolSize=corePoolSize,新任务会被放入阻塞队列等待。(优先使用队列,队列满了再看maxmumPoolSize)
(c)如果阻塞队列的容量达到上限,且这时poolSize<maximumPoolSize,新增线程来处理任务。
(d)如果阻塞队列满了,且poolSize=maximumPoolSize,那么线程池已经达到极限,会根据饱和策略RejectedExecutionHandler拒绝新的任务。
(3)keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
(4)unit:参数keepAliveTime的时间单位,取值如:TimeUnit.DAYS、TimeUnit.HOURS等
(5)workQueue:一个阻塞队列,用来存储等待执行的任务,阻塞队列有以下几种选择:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。线程池的排队策略与workQueue有关。
(6)threadFactory:线程工厂,主要用来创建线程;
(7)handler:表示当拒绝处理任务时的策略,有以下四种取值:
□ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
□ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
□ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
□ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
corePool -> 核心线程池
maximumPool -> 线程池
BlockQueue -> 队列
RejectedExecutionHandler -> 拒绝策略
三、ExecuteService
ExecuteService是一个接口,ThreadPoolExecutor类实现了这个接口,这个接口里面包含了线程池的各种控制操作,具体有如下函数。
public interface ExecutorService extends Executor {
/**
* 关闭线程池,已提交的任务继续执行,不接受新的任务提交
*/
void shutdown();
/**
* 关闭线程池,尝试关闭正在执行的任务,并且不再接受新的任务提交
* 比前面多了一个now,区别在于它可以去停止当前正在执行的任务
*/
List<Runnable> shutdownNow();
/**
* 线程池是否已经关闭了
*/
boolean isShutdown();
/**
* 如果调用了shutdown和shutdownnow方法后,所有任务都结束了,那么返回true
* 该方法必须在调用了shutdown和shutdownNow方法之后调用才会返回true
*/
boolean isTerminated();
/**
* 请求关闭、发生超时或者当前线程中断,无论哪一个首先发生之后,都将导致阻塞,直到所有任务完成执行。
* 等待所有任务完成,并设置了超时时间
* 实际中是:先调用shutdown和shutdownNow,然后再调用该方法等待所有线程真正完成,最后的返回值表示是否超时
*/
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
/**
* 提交一个Callable任务
* 返回一个表示任务的 Future。该 Future 的 get 方法在成功完成时将会返回该任务的结果。
*/
<T> Future<T> submit(Callable<T> task);
/**
* 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
* 该 Future 的 get 方法在成功完成时将会返回给定的结果。
* 因为Runable没有返回值,所有我们制定第二个参数来作为返回值
*/
<T> Future<T> submit(Runnable task, T result);
/**
* 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
* 该 Future 的 get 方法在成功 完成时将会返回 null。
*/
Future<?> submit(Runnable task);
/**
* 执行所有任务,返回Future类型的一个list
*/
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
/**
* 执行给定的任务,当所有任务完成或超时期满时(无论哪个首先发生),返回保持任务状态和结果的 Future 列表。
* 给invokeAll设置了一个超时时间
*/
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
/**
* 只要其中的任意一个任务结束了就可以返回,返回是那个任务的结果
*/
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
/**
* 跟上面方法一样,带超时时间,超过超时时间,则抛出TimeoutException异常
*/
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
以下的几个函数都是线程池的函数,所以调用方式是 线程池名.方法()
1.关闭线程池的函数是:
(1)shutdown
(2)shutdownNow 比前面多了一个now,区别在于它可以去停止当前正在执行的任务,返回值是关闭的任务列表。
2.提交任务
(1)submit 返回一个Future对象,Future 提供了get方法可以取得方法执行完的结果。如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。
(2)execute 没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。
注意:execute方法的参数是Runnable对象,也就是新建的线程:一个Thread类对象
submit的方法参数可以是Runnable对象,也可以Callable对象。
3.Callable和Runnable
public interface Callable<V> {
V call() throws Exception;
}
public interface Runnable {
public abstract void run();
}
(1)Runnable没有返回值;Callable可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
(2)Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛。
4.Future和FutureTask
(1)Future是一个接口,FutureTask是RunnableFuture接口的实现类,RunnableFuture接口集成了Runnable和Future接口。一般用FutureTask对象给Future赋值。
(2)Future和Futuretask的get()方法的返回值是线程执行结果,同时这个方法也可以抛出异常。
四、Executor类
ThreadPoolExecutor是表示一个线程池,而Executors则是线程工厂的角色,通过Executors,我们可以得到一个拥有特定功能的线程池。
Executor工具类提供了以下几种静态的构造线程池的方法:
newFixedThreadPool(int nThreads)
//创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。
newWorkStealingPool()
//创建持有足够线程的线程池来支持给定的并行级别,并通过使用多个队列,减少竞争,
//它需要穿一个并行级别的参数,如果不传,则被设定为默认的CPU数量。
newSingleThreadExecutor()
//该方法返回一个固定数量的线程池
//该方法的线程始终不变,当有一个任务提交时,若线程池空闲,则立即执行,
//若没有,则会被暂缓在一个任务队列只能怪等待有空闲的线程去执行。
newCachedThreadPool()
//返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,
//若有空闲的线程则执行任务,若无任务则不创建线程,并且每一个空闲线程会在60秒后自动回收。
newScheduledThreadPool(int corePoolSize)
//返回一个SchededExecutorService对象,但该线程池可以设置线程的数量,
//支持定时及周期性任务执行。
newSingleThreadScheduledExecutor()
//创建一个单例线程池,定期或延时执行任务。
1.newFixedThreadPool() 无界线程池,阻塞队列是无界的
该方法创建指定线程数量的线程池,没有限制可存放的线程数量(无界队列),适用于线程任务执行较快的场景。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
核心线程数和是最大线程数都是传入的参数,存活时间是0,时间单位是毫秒,阻塞队列是无界队列LinkedBlockingQueue。由于队列采用的是无界队列LinkedBlockingQueue,最大线程数maximumPoolSize和keepAliveTime都是无效参数,拒绝策略也将无效。
当线程池中的任务处理不及时的时候,而一边又疯狂的提交任务,将会导致OOM发生(OutOfMemoryError,内存溢出)。
2.newCachedThreadPool 可缓存的线程池,corepoolsize是0但是max是整数最大值
newCachedThreadPool方法创建的线程池会根据需要自动创建新线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newCachedThreadPool方法也是返回ThreadPoolExecutor对象,核心线程是0,最大线程数是Integer的MAX_VALUE,也就是整数的最大值,存活时间是60,时间单位是秒,SynchronousQueue队列。
该方法将返回一个可根据实际情况调整的线程数量的线程池,线程池的数量不固定,我们可以看见上面的方法中设置的是corePoolSize为0,maximumPoolSize为整数最大值,保活时间为60秒,阻塞队列为SynchronousQueue,故线程池中有空闲线程可以复用的话,则会优先复用空闲线程,如果所有的线程都在工作的话,新的任务提交,直接会创建新的线程处理任务,所有线程处理完任务后,将会返回线程池进行复用。如果同时又大量任务提交,那么将会开启等量的线程,这样也会导致OOM。
3.newSingleThreadExecutor 单线程池,coresize和maxsize都是1
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
可见这个方法,只会创建一个线程的线程池。多余的任务还是会被添加到 LinkedBlockingQueue中,也会有OOM情况的发生。
- newScheduledThreadPool()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
该方法也返回一个ScheduledExecutorService对象,不过可以指定核心线程的数量。
ScheduleExecutorService继承自ExecutorService,可以根据时间需要对线程进行调度
schedule:会在给定时间,对任务进行一次调度
scheduleAtFixedRate:周期性调度,任务的调度频率是一定的。它是以上一个任务开始执行时间为起点,之后的period时间,调度下一次任务,即period包含了线程执行的时间的。
scheduleWithFixedDelay:周期性调度,在上一个任务结束后,再经过delay时间进行任务调度。
五、拒绝策略
JVM一共提供了四种拒绝策略
1 AbortPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
该策略会直接抛出异常,阻止系统正常工作
2 CallerRunsPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
只要线程未关闭,该策略直接调用者线程中执行将被丢弃的任务,这样的话不会真正抛弃任务,但会影响提交线程的性能。
3 DiscardOldestPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
丢弃最开始的(即将被执行的)任务,并尝试再次提交任务。
4 DiscardPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
该策略将直接丢弃无法处理的任务,不予任何处理
六、自定义线程池的创建
在严谨的开发场合,需要自定义线程池,才能准确控制各种情况。
使用ThreadPoolExecutors的构造函数来构造自己需要的线程池。
public class ThreadFactoryDemo {
public static class MyTask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+System.currentTimeMillis());
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+" "+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyTask myTask=new MyTask();
ExecutorService executorService=new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t=new Thread(r);
t.setDaemon(true);
System.out.println("create"+t.getName());
return t;
}
});
for (int i = 0; i < 5; i++) {
executorService.submit(myTask);
}
Thread.sleep(1000);
executorService.shutdown();
}
}
参数的设定参考如下:
* 需要根据几个值来决定
- tasks :每秒的任务数,假设为500~1000
- taskcost:每个任务花费时间,假设为0.1s
- responsetime:系统允许容忍的最大响应时间,假设为1s
* 做几个计算
(1)corePoolSize 一般设置成平均每秒处理的任务数
* threadcount = 20%taskstaskcout = (500~1000)0.1 = 20%(50~100) 个线程。
20%是根据8020定律得来的,简单理解是80%的情况下,核心的任务数大约占到20%
(2)queueCapacity = (coreSizePool/taskcost)responsetime
* 计算可得 queueCapacity = 80/0.11 = 80。意思是队列里的线程最多可以等待1s,所以需要80长度的阻塞队列,这样一秒钟后队列前面80个任务都完成了,所以1s后就能开始执行了。
(3) maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
* 计算可得 maxPoolSize = (1000-80)/10 = 92
* (最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
(4)rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
(5)keepAliveTime和allowCoreThreadTimeout采用默认通常能满足