目录
概述
Java标准库中为我们提供了几种常用的线程池,创建这些线程池的方法都被封装到Executors工具类中。
- FixedThreadPool: 线程数量固定的线程池,使用Executors.newFixedThreadPool()创建;
- CachedThreadPool:线程数根据任务动态调整的线程池,使用Executors.newCachedThreadPool()创建;
- SingleThreadExecutor: 仅提供一个单线程的线程池,使用Executors.newSingleThreadExecutor()来创建;
- ScheduledThreadPool: 能实现定时,周期性任务的线程池,使用Executors.newScheduledThreadPool来创建。
FixedThreadPool线程池
线程数固定的线程池
下面是FixedThreadPool线程池的使用案例:
public class Main {
public static void main(String[] args) {
// 创建一个固定大小的线程池:
ExecutorService executorService = Executors.newFixedThreadPool(4);
for (int i = 0; i < 6; i++) {
executorService.execute(new Task("线程"+i));
}
// 关闭线程池:
executorService.shutdown();
}
}
class Task implements Runnable {
private String taskName;
public Task(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println("启动线程 ===> " + this.taskName);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("结束线程 <= " + this.taskName);
}
}
执行结果:
启动线程 ===> 线程2
启动线程 ===> 线程3
启动线程 ===> 线程0
启动线程 ===> 线程1
结束线程 <= 线程2
结束线程 <= 线程3
结束线程 <= 线程1
结束线程 <= 线程0
启动线程 ===> 线程5
启动线程 ===> 线程4
结束线程 <= 线程4
结束线程 <= 线程5
结果分析:
由于使用的线程池为固定数量,因此只有前四个线程任务会同时执行,每个任务被分配一个线程。
剩下的两个线程任务只能在阻塞队列中等待,等到有空闲线程的时候才会被分配线程并执行。
CachedThreadPool线程池
线程数根据任务数量动态调整的线程池
使用CachedThreadPool观察运行结果:
ExecutorService executorService = Executors.newCachedThreadPool();
启动线程 => 线程1
启动线程 => 线程5
启动线程 => 线程2
启动线程 => 线程4
启动线程 => 线程0
启动线程 => 线程3
结束线程 <= 线程4
结束线程 <= 线程1
结束线程 <= 线程5
结束线程 <= 线程0
结束线程 <= 线程3
结束线程 <= 线程2
结果分析:
我们看到,由于CachedThreadPool会根据任务数量动态调整线程池的大小,所以6个任务可以同时执行。
ScheduledThreadPool线程池
能实现定时、周期性任务的线程池
- 例如: 每秒刷新证券价格,定期发送短信或者邮件,这种任务本身固定,需要反复执行的,可以使用ScheduledThreadPool;
- 放入ScheduledThreadPool的任务可以定期反复执行。
创建定时任务线程池ScheduledThreadPool :
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
延迟三秒钟后执行,任务只执行1次
executorService.schedule(new Task("线程A"), 3, TimeUnit.SECONDS);
延迟两秒钟后,每隔三秒钟执行任务1次
// 方式1
executorService.scheduleAtFixedRate(new Task("线程A"), 2,3, TimeUnit.SECONDS);
// 方式2
executorService.scheduleWithFixedDelay(new Task("线程A"), 2,3, TimeUnit.SECONDS);
分别由两种延迟方法,它们对执行间隔的定义有所不同:
- FixedRate: 是指任务总是以固定时间间隔触发,不管任务执行多长时间,只要到了设置好的时间,就会再次执行线程任务;
- FixedDelay: 是指在上一次任务执行完毕之后,等待固定的时间间隔,然后再执行下一次任务。
singleThreadPool
仅提供单线程,这样可以保证在多线程的环境下,使线程任务得到一定的顺序执行。
下面是使用singleThreadPool 来执行线程任务的案例:
public class Demo01 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for(int i=0;i<10;i++){
executorService.execute(new TaskThread("线程任务"+i));
}
executorService.shutdown();
}
}
class TaskThread implements Runnable{
private String TaskName;
public TaskThread(String taskName){
this.TaskName = taskName;
}
@Override
public void run() {
System.out.println("线程任务被执行=====>"+ TaskName);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程任务结束=====>"+TaskName);
}
}
执行结果:
我们可以看出,这10个线程任务分别按照顺序执行,这也是 singleThreadPool这个线程池特点的体现。
总结
FixedThreadPool
线程数固定的线程池
线程池参数:
- 核心线程数和最大线程数相等;
- 非核心线程空闲存活时间keepAliveTime为0;
- 阻塞队列为无界队列LinkedBlockingQueue。
CachedThreadPool
可缓存线程池,线程数根据任务动态调整的线程池
线程池参数:
- 核心线程数为0;
- 最大线程数是Integer.MAX_VALUE;
- 工作队列是SynchronousQueue同步队列;
- 非核心线程存活时间为60秒。
工作机制:
- 提交线程任务;
- 因为核心线程数为0,所以直接加到工作队列;
- 判断是否有空闲线程,如果有则将任务取出并执行;
- 如果没有,则新建线程执行任务;
- 执行完任务的空闲线程存活时间为60秒,在这期间还可以处理别的线程任务,超过时间则销毁。
使用场景: 用与并发执行大量短期的小任务。
SingleThreadExecutor
单线程化的线程池
线程池参数:
- 核心线程数和最大线程数为1;
- 阻塞队列是LinkedBlockingQueue;
- 非核心线程空闲存活时间为0秒。
工作机制: 每次处理一个线程任务,其他线程任务在阻塞队列中等待。
使用场景: 适用于串行执行任务的场景,将任务按顺序执行。
ScheduledThreadPool
能定时,周期性的执行线程任务的线程池
线程池参数:
- 最大线程数为Integer.MAX_VALUE;
- 阻塞队列是DelayedWorkQueue;
- keepAliveTime为0。
使用场景: 周期性地执行任务,并且需要限制线程数量的需求场景。
线程池使用注意事项(重要!!!!!!)
在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式。jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过new ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
必须为线程池中的线程,按照业务规则,进行命名。可以在创建线程池时,使用自定义线程工厂规范线程命名方式,避免线程使用默认名称。
不同的线程池其实都是根据new ThreadPool()来传入不同的参数,组合在一起形成了各种不同功能的线程池。