我们一般不会选择直接使用线程类Thread
进行多线程编程,而是使用更方便的线程池来进行任务的调度和管理。线程池就像共享单车,我们只要在我们有需要的时候去获取就可以了。甚至可以说线程池更棒,我们只需要把任务提交给它,它就会在合适的时候运行了。但是如果直接使用Thread
类,我们就需要在每次执行任务时自己创建、运行、等待线程了,而且很难对线程进行整体的管理,这可不是一件轻松的事情。既然我们已经有了线程池,那还是把这些麻烦事交给线程池来处理吧。
这篇文章将会从线程池的概念与一般使用入手,首先让大家可以了解线程池的基本使用方法,之后会介绍实践中最常用的四种线程池。最后,我们会通过对JDK源代码的剖析深入了解线程池的运行过程和具体设计,真正达到知其然而知其所以然的水平。虽然只要了解了API就可以满足一般的日常使用了,但是只有当我们真正厘清了多线程相关的知识点,才能在面对多线程的实践与面试问题时做到游刃有余、成竹在胸。
本文是一系列多线程文章中的第三篇,主要讲解了线程池相关的知识,这个系列总共有十篇文章,前五篇暂定结构如下,感兴趣的读者可以关注一下:
1. 并发基本概念——当我们在说“并发、多线程”,说的是什么?
2. 多线程入门——这一次,让我们完全掌握Java多线程(2/10)
3. 线程池使用与原理剖析——本文
4. 线程同步机制
5. 并发常见问题
1 线程池的使用方法
一般我们最常用的线程池实现类是ThreadPoolExecutor
,我们接下来会介绍这个类的基本使用方法。JDK已经对线程池做了比较好的封装,相信这个过程会非常轻松。
1.1 创建线程池
既然线程池是一个Java类,那么最直接的使用方法一定是new一个ThreadPoolExecutor
类的对象,例如ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>() )
。那么这个构造器的里每个参数是什么意思呢?
下面就是这个构造器的方法签名:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
各个参数分别表示下面的含义:
- corePoolSize,核心线程池大小,一般线程池会至少保持这么多的线程数量;
- maximumPoolSize,最大线程池大小,也就是线程池最大的线程数量;
- keepAliveTime和unit共同组成了一个超时间,
keepAliveTime
是时间数量,unit
是时间单位,单位加数量组成了最终的超时时间。这个超时时间表示如果线程池中包含了超过corePoolSize
数量的线程,则在有线程空闲的时间超过了超时时间时该线程就会被销毁; - workQueue是任务的阻塞队列,在没有线程池中没有足够的线程可用的情况下会将任务先放入到这个阻塞队列中等待执行。这里传入的队列类型就决定了线程池在处理这些任务时的策略。
线程池中的阻塞队列专门用于存放待执行的任务,在ThreadPoolExecutor
中一个任务可以通过两种方式被执行:第一种是直接在创建一个新的Worker时被作为第一个任务传入,由这个新创建的线程来执行;第二种就是把任务放入一个阻塞队列,等待线程池中的工作线程捞取任务进行执行。
上面提到的阻塞队列是这样的一种数据结构,它是一个队列(类似于一个List),可以存放0到N个元素。我们可以对这个队列进行插入和弹出元素的操作,弹出操作可以理解为是一个获取并从队列中删除一个元素的操作。当队列中没有元素时,对这个队列的获取操作将会被阻塞,直到有元素被插入时才会被唤醒;当队列已满时,对这个队列的插入操作将会被阻塞,直到有元素被弹出后才会被唤醒。这样的一种数据结构非常适合于线程池的场景,当一个工作线程没有任务可处理时就会进入阻塞状态,直到有新任务提交后才被唤醒。
1.2 提交任务
当创建了一个线程池之后我们就可以将任务提交到线程池中执行了。提交任务到线程池中相当简单,我们只要把原来传入Thread
类构造器的Runnable
对象传入线程池的execute
方法或者submit
方法就可以了。execute
方法和submit
方法基本没有区别,两者的区别只是submit
方法会返回一个Future
对象,用于检查异步任务的执行情况和获取执行结果(异步任务完成后)。
我们可以先试试如何使用比较简单的execute
方法,代码例子如下:
public class ThreadPoolTest {
private static int count = 0;
public static void main(String[] args) throws Exception {
Runnable task = new Runnable() {
public void run() {
for (int i = 0; i < 1000000; ++i) {
synchronized (ThreadPoolTest.class) {
count += 1;
}
}
}
};
// 重要:创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
// 重要:向线程池提交两个任务
threadPool.execute(task);
threadPool.execute(task);
// 等待线程池中的所有任务完成
threadPool.shutdown();
while (!threadPool.awaitTermination(1L, TimeUnit.MINUTES)) {
System.out.println("Not yet. Still waiting for termination");
}
System.out.println("count = " + count);
}
}
1.3 关闭线程池
上面的代码中为了等待线程池中的所有任务执行完已经使用了shutdown()
方法,关闭线程池的方法主要有两个: