好的,各位观众老爷们,欢迎来到今天的并发工具类专场!今天我们要聊的是Java并发编程中一对形影不离的好基友:ExecutorService和Future。别看名字挺唬人,其实用起来嘛,只要掌握了精髓,那也是相当滴丝滑。准备好了吗?老司机要开车咯! 🚗💨
一、并发编程:一条不归路?
首先,咱们得聊聊并发编程这事儿。为啥要搞并发?很简单,为了更快!想象一下,你吭哧吭哧地用单线程处理一个庞大的任务,那感觉就像蜗牛爬珠穆朗玛峰,慢到让你怀疑人生。
但是,并发编程也不是那么容易驾驭的。它就像一匹野马,跑得快是快,但一不小心就会把你甩下马背,让你摔个狗啃泥。什么线程安全问题、死锁、活锁、饥饿……这些概念就像一群恶魔,随时准备跳出来给你捣乱。😈
所以,我们需要一些趁手的兵器来驯服这匹野马。ExecutorService和Future就是其中两把非常重要的利剑。
二、ExecutorService:线程池大管家
ExecutorService,顾名思义,是一个执行服务的接口。它本质上就是一个线程池管理器,负责管理线程的生命周期,以及提交给它的任务。
想象一下,你开了一家餐馆,需要很多服务员来招待客人。如果每次来一个客人就招一个服务员,客人走了就把服务员辞退,那效率得多低啊!而且,频繁地创建和销毁线程也是非常消耗资源的。
这时候,ExecutorService就派上用场了。它可以预先创建好一批线程,放在线程池里,就像餐馆里常驻的服务员一样。当有任务(客人)来的时候,就从线程池里取一个线程(服务员)来处理任务。任务处理完后,线程不会被销毁,而是回到线程池里等待下一个任务。
这样一来,既提高了效率,又节省了资源,简直完美!👏
1. ExecutorService的常用方法:
| 方法名 | 功能描述 |
|---|---|
execute(Runnable command) |
提交一个Runnable任务给线程池执行。Runnable没有返回值。 |
submit(Callable task) |
提交一个Callable任务给线程池执行。Callable可以有返回值,并且可以抛出异常。返回一个Future对象,可以用来获取任务的执行结果。 |
submit(Runnable task, T result) |
提交一个Runnable任务给线程池执行,并返回一个Future对象,该Future对象在任务完成后会返回指定的result。 |
invokeAll(Collection<? extends Callable<T>> tasks) |
提交一组Callable任务给线程池执行,并返回一个Future对象的列表,每个Future对象对应一个任务的执行结果。 |
invokeAny(Collection<? extends Callable<T>> tasks) |
提交一组Callable任务给线程池执行,只要有一个任务执行成功,就返回该任务的执行结果,并取消其他任务的执行。 |
shutdown() |
停止接受新的任务,但会继续执行已经提交的任务。 |
shutdownNow() |
尝试停止所有正在执行的任务,并停止接受新的任务。返回一个List,包含所有尚未开始执行的任务。 |
isShutdown() |
判断线程池是否已经关闭。 |
isTerminated() |
判断线程池是否已经完全终止。只有在shutdown()或shutdownNow()方法调用后,并且所有任务都执行完毕,才会返回true。 |
awaitTermination(long timeout, TimeUnit unit) |
阻塞当前线程,直到线程池中的所有任务都执行完毕,或者超时。 |
2. 几种常见的线程池:
Java提供了几种预定义的线程池,可以直接拿来用,非常方便:
FixedThreadPool: 固定大小的线程池。线程数量固定,不会动态增加或减少。适合处理CPU密集型任务。CachedThreadPool: 大小不固定的线程池。线程数量可以动态增加,当线程空闲一段时间后,会被回收。适合处理IO密集型任务。SingleThreadExecutor: 只有一个线程的线程池。所有任务都会按照提交的顺序依次执行。ScheduledThreadPool: 可以执行定时任务的线程池。
3. 代码示例:
import java.util.concurrent.*;
public class ExecutorServiceDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务
for (int i = 0; i < 10; i++) {
final int taskNum = i;
Future<String> future = executor.submit(() -> {
System.out.println("Task " + taskNum + " is running in thread: " + Thread.currentThread().getName());
Thread.sleep(1000); // 模拟耗时操作
return "Result of task " + taskNum;
});
// 可以选择性地获取任务的执行结果
try {
System.out.println(future.get()); // 阻塞当前线程,直到任务完成
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
// 关闭线程池
executor.shutdown();
// 等待所有任务执行完毕
executor.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("All tasks finished.");
}
}
这段代码创建了一个大小为5的固定线程池,然后提交了10个任务。每个任务都会打印出当前线程的名称,并模拟一个耗时操作。最后,关闭线程池,并等待所有任务执行完毕。
三、Future:任务的未来,结果的希望
Future接口代表异步计算的结果。当你向ExecutorService提交一个任务时,它会返回一个Future对象。你可以通过Future对象来获取任务的执行结果,或者取消任务的执行。
Future就像一张彩票,你买了彩票,就代表你参与了一次抽奖活动。但是,你现在还不知道中奖结果,你需要等到开奖的时候才能知道。Future也是一样,你提交了一个任务,就代表你发起了一次异步计算。但是,你现在还不知道计算结果,你需要等到任务完成的时候才能知道。
1. Future的常用方法:
| 方法名 | 功能描述 |
|---|---|
get() |
阻塞当前线程,直到任务完成,并返回任务的执行结果。如果任务抛出异常,则会抛出ExecutionException。 |
get(long timeout, TimeUnit unit) |
阻塞当前线程,直到任务完成,或者超时。如果在超时时间内任务没有完成,则会抛出TimeoutException。 |
isDone() |
判断任务是否已经完成。 |
isCancelled() |
判断任务是否已经被取消。 |
cancel(boolean mayInterruptIfRunning) |
尝试取消任务的执行。如果任务已经开始执行,mayInterruptIfRunning参数指定是否中断正在执行的任务。如果任务尚未开始执行,则会直接取消任务。 |
2. 代码示例:
import java.util.concurrent.*;
public class FutureDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newSingleThreadExecutor();
// 提交一个任务,并获取Future对象
Future<Integer> future = executor.submit(() -> {
System.out.println("Task is running in thread: " + Thread.currentThread().getName());
Thread.sleep(2000); // 模拟耗时操作
return 123;
});
// 在任务执行期间,可以做其他事情
System.out.println("Doing something else...");
// 获取任务的执行结果
try {
Integer result = future.get(); // 阻塞当前线程,直到任务完成
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executor.shutdown();
}
}
这段代码提交了一个任务给线程池,并获取了Future对象。在任务执行期间,可以做其他事情。最后,通过future.get()方法获取任务的执行结果。
四、ExecutorService + Future:最佳拍档,天下无敌?
ExecutorService负责管理线程池,Future负责获取任务的执行结果。它们俩就像一对黄金搭档,可以让你轻松地实现异步并发编程。
想象一下,你要下载100个文件,如果用单线程下载,那得等到猴年马月啊!但是,如果用ExecutorService和Future,就可以同时下载多个文件,大大提高下载速度。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class DownloadFiles {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(10); // 创建一个大小为10的线程池
List<Future<String>> futures = new ArrayList<>();
// 提交100个下载任务
for (int i = 0; i < 100; i++) {
final String url = "http://example.com/file" + i + ".txt"; // 假设有100个文件需要下载
Future<String> future = executor.submit(() -> {
System.out.println("Downloading " + url + " in thread: " + Thread.currentThread().getName());
Thread.sleep(500); // 模拟下载过程
return "Downloaded " + url;
});
futures.add(future);
}
// 获取下载结果
for (Future<String> future : futures) {
try {
System.out.println(future.get()); // 阻塞当前线程,直到下载完成
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("All files downloaded.");
}
}
这段代码创建了一个大小为10的线程池,然后提交了100个下载任务。每个任务都会模拟下载一个文件,并返回下载结果。最后,获取所有下载任务的执行结果。
五、使用注意事项:小心驶得万年船!
虽然ExecutorService和Future非常强大,但是在使用的时候也需要注意一些问题:
- 线程池的大小: 线程池的大小需要根据实际情况进行调整。如果线程池太小,会导致任务排队等待,降低效率。如果线程池太大,会导致资源浪费,甚至导致系统崩溃。
- 任务的提交方式: 提交任务时,需要选择合适的提交方式。如果任务不需要返回值,可以使用
execute()方法。如果任务需要返回值,可以使用submit()方法。 - 异常处理: 在异步任务中,需要注意异常处理。如果任务抛出异常,需要及时捕获并处理,否则会导致程序崩溃。
- 线程池的关闭: 当不再需要使用线程池时,需要及时关闭线程池。否则会导致资源泄露。
六、总结:并发编程,任重道远
ExecutorService和Future是Java并发编程中非常重要的工具类。它们可以帮助你轻松地实现异步并发编程,提高程序的效率和性能。但是,并发编程也是一项非常复杂的任务,需要仔细考虑各种问题,才能写出高质量的并发程序。
希望今天的讲解能够帮助你更好地理解ExecutorService和Future,让你在并发编程的道路上走得更远!💪
最后,记住一句至理名言:并发虽好,可不要贪杯哦!🍻