Java并发编程05:线程池
线程池顶层接口
线程池顶级接口Executor
Executor
接口为线程池的顶级接口,其executor()
方法接收一个Runnable
实现类对象,定义了在使用线程池时,如何调用线程中的业务逻辑.
class DirectExecutor implements Executor {
public void execute(Runnable r) {
r.run();
}
}
class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
}
}
线程池接口ExecutorService
ExecutorService
是下面所有线程池的父接口,继承自Executor
.
-
其
submit()
方法接收一个Callable
或Runnable
对象,用于指定线程的行为.返回一个Future
对象,用来取消任务或取得Callable
对象call()
方法的返回值. -
其
shutdown()
方法和shutdownNow()
方法都可以关闭线程池,调用此方法后,线程池不能再调用submit()
shutdown()
方法会等待当前线程池内所有任务全部完成再关闭线程池.shutdownNow()
方法会尝试终止池内正在运行的线程且放弃正在等待的任务,马上关闭线程池.awaitTermination()
方法可以使当前线程阻塞一段时间等待线程池被关闭完.返回一个boolean
指示该线程池是否被关闭完.
void shutdownAndAwaitTermination(ExecutorService pool) { pool.shutdown(); // Disable new tasks from being submitted try { // Wait a while for existing tasks to terminate if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { pool.shutdownNow(); // Cancel currently executing tasks // Wait a while for tasks to respond to being cancelled if (!pool.awaitTermination(60, TimeUnit.SECONDS)) System.err.println("Pool did not terminate"); } } catch (InterruptedException ie) { // (Re-)Cancel if current thread also interrupted pool.shutdownNow(); // Preserve interrupt status Thread.currentThread().interrupt(); }
一个线程池的状态有以下三种:
Running
: 活动状态,线程池中的线程正在运行且可以通过调用submit()
方法接收新任务.ShuttingDown
: 线程池正在关闭过程中,线程池中有线程在执行任务,但不会接收新任务.Terminated
: 线程池已经关闭,此案城池中没有线程在运行,且不接受新任务.
与线程池相关的类
Executors
-线程池的工厂类和工具类
Executors
为线程池的工厂类和工具类,我们使用其newXXXPool()
方法创建各种封装好的线程池.
Callable
- 用于定义任务及其返回值
Callable
类用于创建一个任务,类似于Runnable
接口,其call()
方法定义任务具体执行的行为.与Runnalbe
类的不同之处在于Callable
的call()
方法可以有返回值且支持泛型.
要注意
Callable
和Runnable
定义的都是任务而不是线程,要将其传入一个线程或线程池后才可以执行.
Future
-用于获取任务返回值
每将一个任务(Callable
或Runnable
对象)被剑入线程池后,会返回一个Future
对象.其主要方法和属性如下:
cancel()
方法可以取消该任务.get()
方法可以阻塞当前线程直到该任务执行完毕并获取其返回值.done
属性指示任务是否执行完毕,cancelled
属性指示任务是否被取消.
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 将任务提交给线程池后会返回一个Future,其泛型代表任务方法返回值类型
Future<String> result = Executors.newFixedThreadPool(5).submit(() -> {
TimeUnit.MILLISECONDS.sleep(5000);
return "returnValue";
});
System.out.println(result.isDone()); // result对应的线程没有执行完毕,返回false
System.out.println(result.get()); // 阻塞主线程直到 result对应的线程执行完毕
System.out.println(result.isDone()); //result对应的线程没有执行完毕,返回true
}
运行程序输出如下
false
# 线程会在这里阻塞五秒钟
returnValue
true
Java线程池的具体实现
ThreadPoolExecutor
实现的线程池
ThreadPoolExecutor
为最常见的线程池类,其构造函数如下:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler
);
各参数代表的意义如下:
corePoolSize
: 核心线程数量.maximumPoolSize
: 最大线程数量.keepAliveTime
: 线程的最大存活时间.一个线程超过keepAliveTime
时间不工作则会被销毁.(除非当前池中线程个数小于corePoolSize
)unit
: 枚举类型,表示keepAliveTime
的单位.workQueue
: 存放任务的队列.handler
: 拒绝策略(添加任务失败后如何处理该任务)
线程池的运行策略如下:
- 线程池刚创建时,里面没有任何线程.
- 当调用
execute()
方法添加一个任务时,会根据当前线程池中运行线程个数做出不同行为- 若当前线程池中运行线程数小于
corePoolSize
,则在线程池中添加一个新线程执行该任务,即使当前线程池中有空闲线程. - 若当前线程池中运行线程数大于等于
corePoolSize
,则尝试将这个任务存入任务队列workQueue
.- 若任务队列满了且当前线程池中运行线程数小于等于
maximumPoolSize
,则在线程池中添加一个新线程执行该任务. - 若任务队列满了且当前线程池中运行线程数大于
maximumPoolSize
,则任务将被拒绝并执行handler
中的拒绝策略.
- 若任务队列满了且当前线程池中运行线程数小于等于
- 若当前线程池中运行线程数小于
- 当一个线程完成任务时,它会从队列中取下一个任务来执行.
- 当一个线程超过
keepAliveTime
时间未执行任务时,线程池根据当前线程池中运行线程个数判断是否销毁该线程.若当前线程池中运行线程数大于corePoolSize
,就会销毁该线程,直到线程池收缩到corePoolSize
大小.
FixedThreadPool
:固定容量的线程池
FixedThreadPool
线程池内的最大线程个数是固定的,是一种最常见的线程池实现.通过Executors
类的ExecutorService newFixedThreadPool(int nThreads)
方法创建,其中nThreads
参数指定最大线程数.
在JDK源码中可以查到其创建方法如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
下面程序通过一个容量为4的FixedThreadPool
并行寻找质数:
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(3);
// 向线程池中添加四个任务,分别对四个区间进行查找
Future<List<Integer>> future1 = pool.submit(new ComputeTask(1, 8_0000));
Future<List<Integer>> future2 = pool.submit(new ComputeTask(8_0001, 13_0000));
Future<List<Integer>> future3 = pool.submit(new ComputeTask(13_0001, 17_0000));
Future<List<Integer>> future4 = pool.submit(new ComputeTask(17_0001, 20_0000));
// 主程序阻塞等待四个任务执行完成并获取结果
List<Integer> primes = new LinkedList<>();
primes.addAll(future1.get());
primes.addAll(future2.get());
primes.addAll(future3.get());
primes.addAll(future4.get());
// 关闭线程池
pool.shutdown();
}
// 定义计算任务
static class ComputeTask implements Callable<List<Integer>> {
private int start, end;
ComputeTask(int start, int end) {this.start = start; this.end = end; }
@Override
public List<Integer> call() {
System.out.println(Thread.currentThread().getName() + "start");
List<Integer> returnValue = getPrime(start, end);
System.out.println(Thread.currentThread().getName() + "end");
return returnValue;
}
}
// 寻找[start, end]范围内的质数集合,细节省略
static List<Integer> getPrime(int start, int end) {
// ...
}
我们向容量为3的线程池中加入4个任务,则同一时刻只有3个任务并行执行.程序输出如下,我们发现线程pool-1-thread-3
执行了两个任务.
pool-1-thread-1start
pool-1-thread-2start
pool-1-thread-3start
pool-1-thread-3end
pool-1-thread-1end
pool-1-thread-2end
pool-1-thread-3start
pool-1-thread-3end
CachedThreadPool
:容量自动调整的线程池
CachedThreadPool
线程池中存活的线程数可以根据实际情况自动调整
- 向线程池添加新任务时,优先使用线程池中存活的可用线程;若线程池当前没有可用线程,则向线程池中添加一个新线程
- 若线程池中的线程超过60秒未使用,则回收该线程.
通过Executors
类的ExecutorService newCachedThreadPool()
方法创建.在JDK源码中可以查到其创建方法如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
SingleThreadExecutor
:线程数为1的线程池
SingleThreadExecutor
中的线程个数为1,可以用来保证任务同步执行.通过Executors
类的ExecutorService newSingleThreadExecutor()
方法创建.在JDK源码中可以查到其创建方法如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
示例程序如下:
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
int j = i;
service.execute(() -> {
for (int k = 0; k < 3; k++) {
System.out.println("task" + j + "." + k + " running in " + Thread.currentThread().getName());
}
});
}
}
程序输出如下,我们发现所有任务都被同一个线程同步执行
task0.0 running in pool-1-thread-1
task0.1 running in pool-1-thread-1
task0.2 running in pool-1-thread-1
task1.0 running in pool-1-thread-1
task1.1 running in pool-1-thread-1
task1.2 running in pool-1-thread-1
task2.0 running in pool-1-thread-1
task2.1 running in pool-1-thread-1
task2.2 running in pool-1-thread-1
task3.0 running in pool-1-thread-1
task3.1 running in pool-1-thread-1
task3.2 running in pool-1-thread-1
ScheduledThreadPoolExecutor
实现的线程池
ScheduledThreadPool
:定时执行任务的线程池
ScheduledThreadPool
线程池可以定时执行任务.通过Executors
类的ExecutorService newSingleThreadExecutor(int corePoolSize)
方法创建,其corePoolSize
参数指定线程池的核心线程数.在JDK源码中可以查到其创建方法如下:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
其主要方法有schedule()
,scheduleAtFixedRate()
,scheduleWithFixedDelay()
,可以设定任务的执行计划.可以根据 当前任务的到期情况 自动调整线程池中的线程数.示例程序如下:
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
// scheduleAtFixedRate()使用固定的频率执行某个任务,四个参数:
// command指定执行的任务
// initialDelay指定延时多久第一次执行该任务
// period-指定执行任务的间隔周期
// unit-时间单位
service.scheduleAtFixedRate(() -> {
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}, 0, 500, TimeUnit.MILLISECONDS);
// 我们每隔500毫秒执行一次该任务,然而任务需要1000毫秒才能执行完成
// 因此最终核心线程会满并且会不断向线程池添加新线程
}
ForkJoinPool
实现的线程池
ForkJoinPool
:执行递归任务的线程池
ForkJoinPool
线程池的submit()
方法接收ForkJoinTask
类的任务,该类支持fork()
,join()
成员方法:
fork()
方法会将该子任务加入线程池异步执行.join()
方法会阻塞当前线程直到子任务执行完成并获取其返回值
ForkJoinTask
有两个子类: RecursiveAction
和RecursiveTask
,其中RecursiveAction
任务没有返回值,而RecursiveTask
任务有返回值.它们的任务执行逻辑均写在其compute()
方法中.
ForkJoinPool
是一个较底层的线程池,因而Executor
中没有与其对应的构造方法,需要我们显示调用其构造函数获得该类型的线程池.
public class T {
final static int[] numbers = new int[1000000];
final static int MAX_SIZE = 50000;
final static Random r = new Random();
static {
for (int i = 0; i < numbers.length; i++) {numbers[i] = r.nextInt(1000); }
}
// 累加计算[begin, end]区间和任务
static class AddTask extends RecursiveTask<Long> {
int begin, end; // 计算区间
public AddTask(int begin, int end) {this.begin = begin; this.end = end; }
// 累加计算[begin, end]区间和
protected Long compute() {
if ((end - begin) < MAX_SIZE) {
long sum = 0L;
for (int i = begin; i < end; i++) {sum += numbers[i]; }
return sum;
} else {
int middle = begin + (end - begin) / 2;
// 创建两个子任务并加入线程池
AddTask task1 = new AddTask(begin, middle);
AddTask task2 = new AddTask(middle, end);
task1.fork();
task2.fork();
// 阻塞当前线程直到两个子任务执行完毕
return task1.join() + task2.join();
}
}
}
public static void main(String[] args) throws Exception {
ForkJoinPool pool = new ForkJoinPool();
Future<Long> future = pool.submit(new AddTask(0, numbers.length));
System.out.println(future.get());
}
}
WorkStealingPool
:工作窃取线程池
WorkStealingPool
线程池通过Executors
类的ExecutorService newWorkStealingPool()
方法创建,其核心线程数为机器的核心数.
在JDK源码中可以查到其创建方法如下:
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
WorkStealingPool
线程池采用工作窃取
模式,相比于一般的线程池实现,工作窃取
模式的优势体现在对递归任务的处理方式上.
- 在一般的线程池中,若一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态.
- 而在
工作窃取
模式中,若某个子问题由于等待另外一个子问题的完成而无法继续运行,则处理该子问题的线程会主动寻找其他尚未运行的子问题(窃取过来)来执行.这种方式减少了线程的等待时间,提高了性能.