为了避免重复创建和销毁线程而导致额外的性能开销,JDK 提供了线程池功能来实现线程的复用,具体分为以下几类:
newFixedThreadPool():该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新任务提交时,如果线程池中存在空闲的线程,则立即执行;如果没有,则新任务会被暂时存在一个任务队列中,待有线程空闲时再进行处理。
newSingleThreadExecutor(): 该方法返回一个只有一个线程的线程池。若多个任务被提交到该线程池,则多余的任务会被保存在一个任务队列中,待线程空闲,按照先入先出的顺序被执行。
newCachedThreadPool():根据实际情况动态调整线程数量。当新任务提交时,会优先复用空闲的线程;如果所有线程均处于工作状态,则会创建新的线程来进行处理。
newSingleThreadScheduledExecutor():该方法返回一个 ScheduledExecutorService 对象,线程池大小为 1 。SeheduledExectorService 在继承 ExecutorService 的基础上还额外支持定时任务的执行。
newScheduledThreadPool():与 newSingleThreadScheduledExecutor 方法类似,但可以指定线程池中线程的数量。
线程池的基本使用如下:
public class J1_ThreadPool {
static class Task implements Runnable {
@Override
public void run() {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "正在执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
// 提交任务到线程池
executorService.submit(new Task());
}
// 关闭线程池,此时不再接受新任务,但仍会等待原有的任务执行完成,如果想要立即关闭,则可以使用shutdownNow()
executorService.shutdown();
}
}
上面线程池分类中的 newSingleThreadScheduledExecutor() 和 newScheduledThreadPool() 都可以用于创建支持定时任务的线程池,它们返回的都是 ScheduledExecutorService 接口的实例。ScheduledExecutorService 接口中定义了如下三类定时方法:
/*在给定的时间,对任务进行一次性调度*/
public ScheduledFuture> schedule(Runnable command, long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
/
* 以上一个任务开始执行时间为起点,等待period时间后开始调度下一次任务,
* 如果任务耗时大于period,则上一次任务结束后立即执行下一次任务
*/
public ScheduledFuture> scheduleAtFixedRate(Runnable command,long initialDelay,long period,
TimeUnit unit);
/
* 以上一个任务开始执行时间为起点再经过delay时间后开始调度下一次任务,
* 不论任务耗时如何,上一次任务结束后都需要等待delay时间之后才可以执行下一次任务
*/
public ScheduledFuture> scheduleWithFixedDelay(Runnable command, long initialDelay,long delay,TimeUnit unit);
使用示例如下:
public class J2_ScheduledTask {
private static long cacheTime = System.currentTimeMillis();
static class Task implements Runnable {
private String type;
Task(String type) {
this.type = type;
}
@Override
public void run() {
try {
Thread.sleep(5000);
long nowTime = System.currentTimeMillis();
System.out.println(type + Thread.currentThread().getId() + "执行耗时" + (nowTime - cacheTime) + "毫秒");
cacheTime = nowTime;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 为避免相互间的影响,以下各种场景最好分别测试:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
// 任务只会被执行一次
pool.schedule(new Task("schedule"), 2, TimeUnit.SECONDS);
// 指定2秒为固定周期执行,因为项目执行耗时5秒,此时项目结束会立马执行下一次任务,所以输出的时间间隔为5秒
pool.scheduleAtFixedRate(new Task("FixedRate"), 0, 2, TimeUnit.SECONDS);
// 总是在上一次项目结束后间隔指定周期执行,因为项目耗时5秒,还需要间隔2秒执行,所以输出的时间间隔为7秒
pool.scheduleWithFixedDelay(new Task("WithFixedDelay"), 0, 2, TimeUnit.SECONDS);
// pool.shutdown();
}
}
不管是使用 newFixedThreadPool() 还是使用 newCachedThreadPool() 来创建线程池,其最终调用的都是 ThreadPoolExecutor 的构造器,定义如下:
public ThreadPoolExecutor(int corePoolSize, //核心线程数量
int maximumPoolSize, //最大线程数量
long keepAliveTime, //超过核心线程数的线程的存活时间
TimeUnit unit, //存活时间的单位
BlockingQueue<Runnable> workQueue, //任务队列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler) //拒绝策略
ThreadFactory 用于指定线程的创建方式,示例如下:
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
// 将所有线程都设置为守护线程
thread.setDaemon(true);
System.out.println("create" + thread.getName());
return thread;
}
}
当线程池中可用线程的数量为 0,并且等待队列已满的情况下,线程池需要按照 RejectedExecutionHandler 指定的拒绝策略来决定如何处理后续提交任务,JDK 中默认提供了以下四种拒绝策略:
ThreadPoolExecutor.AbortPolicy:直接拒绝新提交的任务,并抛出异常;
ThreadPoolExecutor.DiscardPolicy:静默拒绝新提交的任务,并不抛出异常;
ThreadPoolExecutor.DiscardOldestPolicy:丢弃等待时间最长的任务,然后再尝试执行新提交的任务;
ThreadPoolExecutor.CallerRunsPolicy:直接在调用 execute 方法的线程中运行新提交的任务。
ThreadPoolExecutor 除了提供丰富的参数来满足多样化的需求外,还支持重载其生命周期方法来进行更加灵活的扩展:
ExecutorService executorService = new ThreadPoolExecutor(10, 20, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>()) {
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("线程" + t.getName() + "准备执行");
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("线程" + r + "执行结束");
}
@Override
protected void terminated() {
System.out.println("线程池退出");
}
};
线程池的大小可以通过以下公式进行估算:
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
W/C = 等待时间与计算时间的比率
为保证处理器达到期望的使用率,最优的线程池的大小等于:
Nthreads = Ncpu x Ucpu x (1+W/C)