1. Java中的线程池
1.1 线程池概述
在计算机编程中,线程池是一种软件设计模式,用于在计算机程序中实现并发执行。线程池维护多个线程,等待监督程序分配任务并并发执行。通过维护线程池,该模型提高了性能,并避免了由于频繁创建和销毁线程而导致的执行延迟。
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步(多线程各自执行各自的)或同步执行任务的程序都可以使用线程池。
在开发过程中,合理地使用线程池的优势如下:
- 降低资源消耗:通过复用已创建的线程来降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
1.2 ExecutorService
ExecutorService是一个Java API,可以简化异步模式下运行的任务(Task)。一般来说,ExecutorService 会自动提供一个线程池和一个用于为其分配任务的API。
ExecutorService名称中的Executor表示一个任务的执行器。开发者可以使用Runnable接口的实现类来封装线程的工作单元(线程启动后执行的具体逻辑),将该工作单元看作一个要被执行的任务,委派给Executor来执行。
ExecutorService接口继承Executor接口,在Executor的基础上扩展了对Executor进行控制的方法:如提供的submit() 方法用于提交Runnable任务,invokeAll()方法用于批量提交任务,shutdown()方法用于停止启动新的任务等。
在Java中,ExecutorService接口的实现类基于线程池来实现,这也是ExecutorService在很多场景下被简单地看成一个线程池工具的原因。常用的ExecutorService接口的实现类包括ThreadPoolExecutor和ScheduledThreadPoolExecutor。
创建ExecutorService实例的方法如下:
- Executors.newSingleThreadExecutor():创建一个使用单个工作线程在无界队列上运行的Executor
- Executors.newFixedThreadPool(int nThreads):创建一个线程池,该线程池复用在共享无界队列上运行的固定数量的线程
- Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,可以配置为在给定延迟后运行,或者定期执行
ExecutorService接口常用的方法如下:
void execute(Runnable command):在将来的某个时间执行给定的任务,根据实际使用的Executor的实现类,该任务可以在新线程、线程池或调用线程中执行。
ExecutorService的使用
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceDemo1 {
public static void main(String[] args) {
// 创建一个包含了4个线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(4);
MyRun1 run1 = new MyRun1(); // 创建任务对象
for(int i =1;i<=4;i++) {
executorService.execute(run1); // 提交4次任务
}
// 关闭线程池,已提交的任务继续执行,不能再提交新的任务
executorService.shutdown();
// executorService.execute(run1); // 抛出RejectedExecutionException
}
}
class MyRun1 implements Runnable {
int num = 0;
@Override
public void run() {
while (true){
synchronized (this){
if (num >1000){
break;
}
String name = Thread.currentThread().getName();
System.out.println(name + ": " + num);
num++;
}
}
}
}
ScheduledExecutorService的使用
import java.time.LocalTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceDemo2 {
public static void main(String[] args) {
// 创建一个包含了4个线程的线程池
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
Runnable run1 = () -> { // 任务1
String name = Thread.currentThread().getName();
System.out.println(name+": "+ LocalTime.now());
};
Runnable run2 = () -> { // 任务2
String name = Thread.currentThread().getName();
System.out.println("====>"+name+": "+ LocalTime.now());
};
// 指定延迟1秒后,每个2秒执行1次任务1
service.scheduleAtFixedRate(run1, 1, 2, TimeUnit.SECONDS);
// 指定延迟2秒后,每个1秒执行1次任务2
service.scheduleAtFixedRate(run2, 2, 1, TimeUnit.SECONDS);
// 任务1和任务2共用线程池中的4个线程
}
}
2. 获取并发任务的结果
2.1 获取并发任务的结果概述
前面的课程中讲述了创建线程的2种方式:一种是直接继承Thread,另外一种是实现Runnable接口。这2种方式都有一个缺陷:在任务执行完成后无法直接获取任务的执行结果。想要获取任务执行结果,必须通过共享变量或者使用线程通信的方式来达到效果,这种实现方式比较复杂。
如上图所示:如果用多线程方式,随机生成100个10以内的数字,并求和,如何确定执行了100次,是个难题。解决办法是:用变量 count 来记载执行的次数,并不断查询变量 count 的值。这种实现方式比较麻烦,而且在解决复杂业务时,代码复杂度也会直线上升。
2.2 Callable接口
为简化获取并发任务结果的编码, Java 5版本中增加了Callable接口和Future接口,通过它们可以在并发任务执行完毕之后便捷的得到任务执行结果。
使用Callable接口被称为Java中创建线程的第三种方式。
Callable接口与Runnable接口相似,代表线程的工作单元,该接口中只有一个call()方法,含义与run()方法相似。
与run()方法不同的是,call()方法可以返回指定的泛型类对象,并且可以抛出Exception类及其子类异常。call()方法的设计为返回并发任务的结果提供了可能。
2.3 Future接口
Future接口表示异步计算的结果,它提供了检查计算是否完成、等待其完成以及检索计算结果的方法:
- cancel()方法:用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false
- isCancelled()方法:表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true
- isDone()方法:表示任务是否已经完成,若任务完成,则返回true
- get()方法:用来获取执行结果,这个方法会产生阻塞,一直等到任务执行完毕才返回执行结果
- get(long timeout, TimeUnit unit)方法:用来获取执行结果,如果在指定时间内没能获取到结果,直接返回null
import java.util.Random;
import java.util.concurrent.*;
public class CallableDemo1 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
MyCall cd = new MyCall();
Future<Integer> future1 = service.submit(cd); // 提交任务
System.out.println("isDone(): "+future1.isDone());
try {
int result = future1.get();
System.out.println("get(): "+result);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("isDone(): "+future1.isDone());
// 关闭ExecutorService
service.shutdown();
}
}
class MyCall implements Callable<Integer> {
Random random = new Random();
int count =1; // 控制计算次数
int result = 0; // 保存计算结果
public Integer call(){
while(true){
synchronized (this){
if (count>100){ // 计算100次
break;
}
int num = random.nextInt(10);
result += num;
String name = Thread.currentThread().getName();
// 输出多线程执行的过程
System.out.println(name+", num="+num+",result="+result);
count++;
}
Thread.yield();
}
return result;
}
}
2.4 FutureTask
FutureTask 为 Future接口提供了基础实现,如get()方法的具体逻辑、cancel()方法的具体逻辑以及其他。
FutureTask常用来封装Callable和Runnable,也可以作为一个任务提交到线程池中执行。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class FutureTaskDemo1 {
public static void main(String[] args) {
int numStudents = 3;
FutureTask<Integer>[] ftArray = new FutureTask[numStudents];
for (int i = 0; i < numStudents; i++) {
Student student = new Student("Student " + (i + 1));
FutureTask<Integer> ft = new FutureTask<>(student);
ftArray[i] = ft;
new Thread(ft).start();
}
// 计算学生平均分
int totalScore = 0;
for (int i = 0; i < numStudents; i++) {
try {
totalScore += ftArray[i].get();
} catch (Exception e) {
e.printStackTrace();
}
}
double averageScore = (double) totalScore / numStudents;
System.out.println("所有学生的平均分为: " + averageScore);
}
static class Student implements Callable<Integer> {
private final String name;
public Student(String name) {
this.name = name;
}
@Override
public Integer call() throws Exception {
// 模拟学生考试
int score = (int) (Math.random() * 100);
System.out.println(name + "结束考试,分数为:" + score);
return score;
}
}
}
3. 总结
1. 线程池
- 在计算机编程中,线程池是一种软件设计模式,用于在计算机程序中实现并发执行
- 线程池维护多个线程,等待监督程序分配任务并并发执行
- 通过维护线程池,提高性能并避免由于频繁创建和销毁线程而导致的执行延迟
2. ExecutorService 是一个JDK API,可以简化异步模式下运行的任务(Task)
- 一般来说,ExecutorService 会自动提供一个线程池和一个用于为其分配任务的API
3. Callable 接口和 Future 接口:在并发任务执行完毕之后便捷的得到任务执行结果
- 使用Callable接口被称为Java中创建线程的第三种方式