Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作:
(1)必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。
(2)需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程。
Java高并发应用频繁创建和销毁线程的操作是非常低效的,而且是不被编程规范所允许的。如何降低Java线程的创建成本?必须使用到线程池。线程池主要解决了以下两个问题:
(1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建的线程进行复用,使得性能提升明显。
(2)线程管理:每个Java线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。
一、使用Executors快捷创建线程池
Executors类提供了五种(原来是四种,java1.8中新增了一种newWorkStealingPool)快捷创建线程池的方式:newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool、newScheduledThreadPool、newWorkStealingPool。
1、newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(
1, //corePoolSize
1, //maximumPoolSize
0L, //keepAliveTime
TimeUnit.MILLISECONDS, //TimeUnit
new LinkedBlockingQueue<Runnable>() //BlockingQueue
));
}
该方法用于创建一个“单线程化线程池”,也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。
案例
public static void newSingleThreadExecutorDemo() {
AtomicInteger count = new AtomicInteger(0);
ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
log.info("任务[{}]开始执行", count.incrementAndGet());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
log.error("", e);
}
log.info("任务[{}]结束执行任务", count.get());
});
}
executor.shutdown();
}
以上代码运行结果如下:

可以看到所有任务都是由一个线程执行,而且所有任务按照入队的顺序依次执行。
特点
从以上输出中可以看出,该线程池有以下特点:
(1)单线程化的线程池中的任务是按照提交的次序顺序执行的。
(2)池中的唯一线程的存活时间是无限的。
(3)当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的(LinkedBlockingQueue)。
2、newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads, //corePoolSize
nThreads,//maximumPoolSize
0L, //keepAliveTime
TimeUnit.MILLISECONDS,//TimeUnit
new LinkedBlockingQueue<Runnable>()//BlockingQueue
);
}
该方法用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。
案例
public static void newFixedThreadPoolDemo() {
AtomicInteger count = new AtomicInteger(0);
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
log.info("任务[{}]开始执行", count.incrementAndGet());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
log.error("", e);
}
});
}
executor.shutdown();
}
以上代码运行结果如下所示:

在测试用例中,创建了一个线程数为2的“固定数量线程池”,然后向其中提交了10个任务。从输出结果可以看到,该线程池同时只能执行2个任务,剩余的任务会排队等待。
特点
“固定数量的线程池”的特点大致如下:
(1)如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
(2)线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
(3)在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。
“固定数量的线程池”的适用场景:需要任务长期执行的场景。“固定数量的线程池”的线程数能够比较稳定地保证一个数,能够避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能少地分配线程。
“固定数量的线程池”的弊端:内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。
3、newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0,//corePoolSize
Integer.MAX_VALUE,//maximumPoolSize
60L, //keepAliveTime
TimeUnit.SECONDS,//TimeUnit
new SynchronousQueue<Runnable>()//BlockingQueue
);
}
该方法用于创建一个“可缓存线程池”,如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。
案例
public static void newCachedThreadPoolDemo() {
AtomicInteger count = new AtomicInteger(0);
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
log.info("任务[{}]开始执行", count.incrementAndGet());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
log.error("", e);
}
});
}
executor.shutdown();
}
以上代码运行结果如下所示:

特点
“可缓存线程池”的特点大致如下:
(1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
(2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
(3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。
“可缓存线程池”的适用场景:需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。
“可缓存线程池”的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。
4、newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(
corePoolSize, //corePoolSize
Integer.MAX_VALUE, //maximumPoolSize
0, //keepAliveTime
NANOSECONDS,//TimeUnit
new DelayedWorkQueue()//BlockingQueue
);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
newScheduledThreadPool方法用于创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。
案例
ScheduledExecutorService有两个重要的方法:scheduleAtFixedRate以及scheduleWithFixedDelay方法。
scheduleAtFixedRate:以固定时间间隔执行任务,如果前次任务未执行完,等待前次任务执行完毕后执行下次任务
scheduleWithFixedDelay:前次任务执行完毕之后,间隔固定时间后执行下一次任务
下面通过scheduleWithFixedDelay演示ScheduledExecutorService用法:
public static void newScheduledThreadPoolDemo() throws InterruptedException {
AtomicInteger count = new AtomicInteger(0);
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
for (int i = 0; i < 2; i++) {
int finalI = i;
executor.scheduleWithFixedDelay(
() -> {
log.info("{}任务[{}]开始执行", finalI, count.incrementAndGet());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
log.error("", e);
}
},
0,
1,//任务执行完成以后等待一秒钟执行下一个任务
TimeUnit.SECONDS
);
}
TimeUnit.SECONDS.sleep(600);
executor.shutdown();
}
上述代码运行结果:

可以看到,执行完scheduleWithFixedDelay方法,任务和任务之间间隔了3秒钟,其中2秒钟是线程执行任务的时间,1秒钟是设置的间隔时间。
特点
“可调度线程池”的适用场景:周期性地执行任务的场景。
5、newWorkStealingPool
newWorkStealingPool方法创建线程池是Java8 新增的,内部使用了ForkJoinPool线程池,它通过“分而治之”的思想运作,并有独特的“工作窃取”算法优化任务排队逻辑。
newWorkStealingPool方法支持传入“并行度”参数,并行度默认值是Runtime.getRuntime().availableProcessors()
也就是当前处理器的核心数,为了维持该并行度,线程数量会动态的增加或者减少。
案例
public static void newWorkStealingPoolDemo() throws InterruptedException {
AtomicInteger count = new AtomicInteger(0);
//这里设置并行度为2,表示设置同时让两个任务并行运行
ExecutorService executor = Executors.newWorkStealingPool(2);
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
log.info("任务[{}]开始执行", count.incrementAndGet());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
log.error("", e);
}
});
}
TimeUnit.SECONDS.sleep(600);
executor.shutdown();
}
其结果运行如下:

特点
适合处理I/O密集型任务,因为当某个线程在执行I/O操作时,其他线程可以窃取任务执行,减少了线程的等待时间。
二、线程池分类
1、线程池类图
上面讲了五种快捷创建线程池的方式,五种创建线程池的方式之间是否存在关联关系?先看五个线程池的类图

2、线程池分类
从线程池类图中可以看到,五种快捷创建的线程池共分为四大类:
ThreadPoolExecutor、FinallzableDelegatedExecutorService、ScheduledThreadPoolExecutor、ForkJoinPool,每一种都比较复杂,都值得仔细探究下其实现。
从下一章开始将会仔细分析每一种线程池实现。
注:本篇文章大量节选自《Java高并发核心编程 卷2:多线程、锁、JMM、JUC、高并发设计模式》一书中内容。
END.
注意:本文归作者所有,未经作者允许,不得转载