Java并发工具类:ExecutorService与Future

好的,各位观众老爷们,欢迎来到今天的并发工具类专场!今天我们要聊的是Java并发编程中一对形影不离的好基友:ExecutorServiceFuture。别看名字挺唬人,其实用起来嘛,只要掌握了精髓,那也是相当滴丝滑。准备好了吗?老司机要开车咯! 🚗💨

一、并发编程:一条不归路?

首先,咱们得聊聊并发编程这事儿。为啥要搞并发?很简单,为了更快!想象一下,你吭哧吭哧地用单线程处理一个庞大的任务,那感觉就像蜗牛爬珠穆朗玛峰,慢到让你怀疑人生。

但是,并发编程也不是那么容易驾驭的。它就像一匹野马,跑得快是快,但一不小心就会把你甩下马背,让你摔个狗啃泥。什么线程安全问题、死锁、活锁、饥饿……这些概念就像一群恶魔,随时准备跳出来给你捣乱。😈

所以,我们需要一些趁手的兵器来驯服这匹野马。ExecutorServiceFuture就是其中两把非常重要的利剑。

二、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个文件,如果用单线程下载,那得等到猴年马月啊!但是,如果用ExecutorServiceFuture,就可以同时下载多个文件,大大提高下载速度。

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个下载任务。每个任务都会模拟下载一个文件,并返回下载结果。最后,获取所有下载任务的执行结果。

五、使用注意事项:小心驶得万年船!

虽然ExecutorServiceFuture非常强大,但是在使用的时候也需要注意一些问题:

  • 线程池的大小: 线程池的大小需要根据实际情况进行调整。如果线程池太小,会导致任务排队等待,降低效率。如果线程池太大,会导致资源浪费,甚至导致系统崩溃。
  • 任务的提交方式: 提交任务时,需要选择合适的提交方式。如果任务不需要返回值,可以使用execute()方法。如果任务需要返回值,可以使用submit()方法。
  • 异常处理: 在异步任务中,需要注意异常处理。如果任务抛出异常,需要及时捕获并处理,否则会导致程序崩溃。
  • 线程池的关闭: 当不再需要使用线程池时,需要及时关闭线程池。否则会导致资源泄露。

六、总结:并发编程,任重道远

ExecutorServiceFuture是Java并发编程中非常重要的工具类。它们可以帮助你轻松地实现异步并发编程,提高程序的效率和性能。但是,并发编程也是一项非常复杂的任务,需要仔细考虑各种问题,才能写出高质量的并发程序。

希望今天的讲解能够帮助你更好地理解ExecutorServiceFuture,让你在并发编程的道路上走得更远!💪

最后,记住一句至理名言:并发虽好,可不要贪杯哦!🍻

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注