目录
为什么要使用线程池?
Java中的线程池
workQueue参数分析
handler参数分析
execute方法与submit方法
合理的选择线程池大小
对于java初学者来说,首先接触到的创建线程的方法就是new Thread,或者实现Runnable接口,重写run方法来实现多线程。虽然简单,但是一句话:谁来帮你管理线程???
若不使用线程池:
1、线程的创建销毁都要自己来完成
2、没有统一的管理,若每次请求都开启多个线程,无限制的请求袭来,可能造成资源耗尽
3、不够灵活
线程池的出现能让你双手游离于多线程之外专注于其他代码,帮你管理线程。所以如果是一个大型系统,建议不论何种场景,都直接使用线程池。
举个例子:你的面前有三台电脑,你可能同时用三台,也可能同时用两台或者一台。但是分配给你的电脑每次都是随机的。假设三台电脑编号123,第一次:【你要用1台分配到1号,然后开机->使用->关机】,第二次:【你要用1台分配到2号,然后开机->使用->关机】,以此类推。假设你的使用时间是10秒,而开机关机是20秒,然后这个过程要重复100次,每次都随机分配。那么无疑开机和关机的过程浪费的大量的时间和资源。
每次使用一个线程,都要经历三个步骤:创建线程->使用线程->销毁线程,类似上述使用场景,频繁的使用线程,频繁的开启和关闭,频繁的浪费时间和资源,额...........如果这时候你面前的三台电脑虽然每次都随机分配,但是却从不关机,坐那就能用,就好了,yes!线程池就是完成了这样的操作!包括:数据库连接池也是同样的道理。线程池中每次创建的线程,如果执行完毕,不会立即进行销毁,而是处于等待状态,下一个任务来了以后无需开启直接使用,方便快捷!!!【线程池可以使已经开启的线程长期处于激活状态,节省创建和销毁线程的时间,实现线程复用!】
java中,Executor是java.util.concurrent包下的一个线程池的鼻祖接口,只有一个抽象方法execute;ExecutorService仍然是一个接口,extends Executor,新增了一些接口例如submit;AbstractExecutorService是一个抽象类,implements ExecutorService;ThreadPoolExecutor是我们最常使用的线程池类,extends AbstractExecutorService;
Executors类提供了四中创建线程池的方法,源码如下,如果还不能满足你,那你只能自定义线程池了。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
package java.util.concurrent.ScheduledThreadPoolExecutor
extends ThreadPoolExecutor implements ScheduledExecutorService----->>>>
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
newFixedThreadPool:创建一个固定大小的线程池,提交一个任务就在线程池中创建一个线程,直到线程数量达到线程池最大限制。线程池一旦到最大就不会再改变,除非有已经开启的线程出现异常,再提交任务时会继续新建线程。这个创建线程池的方法是在实际项目中比较常用的,由于corePoolSize和maximumPoolSize两个参数是相同大小,所以到了线程池最大容量后,如果有任务完成让出占用线程,那么此线程就会一直处于等待状态,而不会消亡,直到下一个任务再次占用该线程。此方法的弊端是:使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无线增大,使服务器资源迅速耗尽。
newCachedThreadPool:创建一个可根据实际情况调整大小的线程池,线程数量不确定,只要有空闲线程空闲时间超过keepAliveTime,就会干掉,再来新任务,先使用空闲线程,若不够,再新建线程。线程池没有最大线程数量限制,所以当大量线程蜂拥而至,会造成资源耗尽。
newSingleThreadExecutor:创建一个容量为1的线程池,被提交任务按优先级依次执行。
newScheduledThreadPool:创建一个定长线程池,长度为输入参数自定义,支持定时周期任务执行,可根据时间需要在指定时间对线程进行调度。
可以看到 newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor均使用的ThreadPoolExecutor构造器,下面分析该构造方法的入参:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);走的是该类中另一个构造方法:如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue 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;
}
corePoolSize:线程池中的线程数量,超过该数量的空闲线程空闲时间超过keepAliveTime则会被销毁,在该数量内的空闲线程则不会被销毁,在下一个任务来临之前一直处于等待状态。
maximumPoolSize:线程池支持的最大线程数,超过该数量则排队等待。
keepAliveTime:超过corePoolSize的空闲线程的存活时间。
unit:keeyAliveTime的单位。
workQueue:任务队列,当提交的任务超过线程池支持的最大线程数,则进入该队列排队等待。该队列的选择会对线程池的性能有重大影响。
threadFactory:线程工厂,用于创建线程,一般使用默认,也可自定义。
handler:当提交任务数量超过线程池容量,并且超过排队队列容量后的拒绝策略。
workQueue是一个BlockingQueue接口的实现类,存放Runnable对象
一般来说,该队列的选择常用如下四种:
(1)SynchronousQueue:【直接提交的队列】,容量为0,每次插入都必须等待一个任务的删除操作,每次删除也要等待一个任务的插入操作。使用该队列,由于容量为0,其不会真实的保存任务,而是每次都将新任务提交给线程池,如果线程池满了,则直接执行拒绝策略,简单粗暴。
(2)ArrayBlockingQueue:【有界的任务队列,按照先进先出的顺序出队】,该队列的构造方法为public ArrayBlockingQueue(int capacity),初始化时必须设置其容量。当线程池中的实际线程数小于corePoolSize,则优先创建新的线程;如果大于corePoolSize,并且此时恰好没有空闲线程,则优先进入有界队列,队列满了以后,再从队列中出队任务创建线程,直到线程池中的线程数量达到maximumPoolSize。当大maximumPoolSize后,执行拒绝策略。
(3)LinkedBlockingQueue:【无界的任务队列,按照先进先出的顺序出队】,当线程池中的实际线程数小于corePoolSize,则优先创建新的线程;如果大于corePoolSize,并且此时恰好没有空闲线程,只要服务器资源足够多,就会无限制的进入入队操作,直到资源耗尽。所以使用了该任务队列的线程池最大支持线程数为corePoolSize,所以使用该队列的创建线程池的方法一般是corePoolSize和maximumPoolSize相等的newFixedThreadPool。
(4)PriorityBlockingQueue:【优先任务队列,按照任务的优先级出队】,带有任务执行优先级的队列,可以控制任务执行的先后顺序,在确保系统性能的同时,对质量也有了保证。
丢弃策略是系统超负荷运行的最后补救措施,线程池满了,并且阻塞队列满了,丢弃策略有如下四种:
(1)ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException异常,丢弃任务,阻止系统正常工作。 【Executors类创建线程池的默认丢弃策略】
(2)ThreadPoolExecutor.DiscardPolicy:偷偷的丢弃任务,但是不抛出异常。
(3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃最老的任务(即队首马上要执行的任务),然后重新尝试执行任务(重复此过程)
(4)ThreadPoolExecutor.CallerRunsPolicy:只要线程池未关闭,直接去调用线程池的线程中处理该任务,可能引起性能的急剧下降。
前面说了,Executor是java.util.concurrent包下的一个线程池的鼻祖接口,只有一个抽象方法execute,该接口所有源码如下,execute方法入参为Runnable command。
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
该方法实际上是在ThreadPoolExecutor类中首次得到了实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个Runnable任务。所以我们在使用execute方法时,都要new Runnable。
submit()方法是在ExecutorService中声明的方法,如下:
/**
* Submits a Runnable task for execution and returns a Future
* representing that task. The Future's {@code get} method will
* return {@code null} upon successful completion.
*
* @param task the task to submit
* @return a Future representing pending completion of the task
* @throws RejectedExecutionException if the task cannot be
* scheduled for execution
* @throws NullPointerException if the task is null
*/
Future> submit(Runnable task);
在AbstractExecutorService类中首次进行了实现,在ThreadPoolExecutor中继承下来并且没有重写,如下:
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public Future> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
submit方法也是用来向线程池提交Runnable任务的,并且最终调用了execute方法,通过Future来获取任务执行结果。
execute和submit方法的不同点:
(1)execute没有返回值;而submit有返回值,方便返回执行结果。
(2)submit方便进行Exception处理,由于返回参数是Future,如果执行期间抛出了异常,可以使用Future.get()进行捕获。
确定线程池的大小需要考虑CPU总核数,《Java Concurrency in Pracitice》书中给出了计算线程池的经验公式:
CPU核数 * CPU核数 *(1+等待时间 / 计算时间),在java中,Runtime.getRuntime().availableProcessors()可以获取当前机器的CPU核数。