多线程实战

208 阅读9分钟

多线程实战

批量控制设备是一个常见的需求。随着设备数量的增加,同步处理请求可能会导致性能瓶颈。为了提高系统的响应速度和处理能力,异步处理成为了一种有效的解决方案。Java 8引入的CompletableFuture类为异步编程提供了一个强大的工具。本文将介绍如何使用CompletableFuture来实现批量控制设备的功能。


比较Future和CompletableFuture

在Java中,FutureCompletableFuture都是用于处理异步任务的接口和类,但它们在功能和使用方式上有一些重要的区别。本文将详细介绍这两者的区别,并结合实际例子说明如何使用CompletableFuture来实现更强大的异步处理功能。

特性FutureCompletableFuture
异步操作不支持异步操作,需要通过get方法阻塞等待结果。提供丰富的异步操作方法,如thenApplythenAcceptthenRun等。
错误处理通过get方法捕获ExecutionException提供exceptionally方法,更方便地处理异常
组合任务不支持任务的组合,需要手动管理多个Future对象。提供thenComposeallOfanyOf等方法,轻松组合多个异步任务。
链式调用不支持链式调用。支持链式调用,可以方便地组合多个异步操作结果获取
结果获取通过get方法获取结果,会阻塞当前线程。通过thenApplythenAccept等方法异步获取结果,不会阻塞。
回调机制不支持回调机制。支持回调机制,可以在任务完成时执行特定的操作。
创建方式通过ExecutorService.submit方法创建。提供多种静态方法,如runAsyncsupplyAsync等。
线程管理需要手动管理线程池。内部使用ForkJoinPool,简化了线程管理。

代码用例

为了避免使用内置线程池造成的内存泄露,在这里使用自定义的线程池。代码如下:

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
    List<String> list=new ArrayList<>();
    list.add("1");
    list.add("2");
    list.add("3");
    list.add("4");
    list.add("5");
    list.add("6");
    list.add("7");
    list.add("8");
    list.add("9");
    list.add("10");
    list.add("11");
    ThreadPoolExecutor executor = new ThreadPoolExecutor( 10, 10,
            60L, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy());
    Map<String, CompletableFuture<String>> futures = new ConcurrentHashMap<>();
    for (String xo : list) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                if (xo.equals("11")){
                    System.out.println("start 11");
                    Thread.sleep(5000);
                }else {
                    System.out.println("start other");
                    Thread.sleep(1000);
                }
                System.out.println(xo+" done");

                return xo+" out done";
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }, executor);
        futures.put(xo,future);
    }
    CompletableFuture.allOf(futures.values().toArray(new CompletableFuture[0])).get(30, TimeUnit.SECONDS);
    for (Map.Entry <String, CompletableFuture<String>> entry : futures.entrySet()){
        System.out.println("=>"+entry.getValue().get());
    }
    System.out.println("all done");
}
  • 使用List模拟设备。
  • 创建一个固定大小的线程池,最大线程数为10。线程空闲时间60秒。使用ArrayBlockingQueue作为任务队列,队列大小为100。使用CallerRunsPolicy作为拒绝策略,当任务队列满时,由调用线程执行任务。
  • 使用ConcurrentHashMap存储每个设备ID对应的CompletableFuture。遍历设备列表,为每个设备创建一个CompletableFuture任务。使用supplyAsync方法异步执行任务,指定线程池executor。在任务中,根据设备ID模拟不同的操作时间:设备ID为"11"的设备模拟5秒的操作时间。其他设备模拟1秒的操作时间。任务完成后,返回结果:xo + " out done"。
  • 遍历futures映射,获取每个任务的结果并打印。

欢迎指正代码

知识点

线程池的七大参数

线程池的创建方式:

ThreadPoolExecutor executor = new ThreadPoolExecutor( 10,//corePoolSize
			10,//maximumPoolSize
            60L,//keepAliveTime
			TimeUnit.SECONDS,//unit
            new ArrayBlockingQueue<>(100),//workQueue
			new ThreadFactory() { //threadFactory
                @Override
                public Thread newThread(Runnable r) {
                    Thread t = new Thread(r);
                    t.setName("MyThreadPool-Thread-" + t.getId());
                    return t;
                }
            }
            new ThreadPoolExecutor.CallerRunsPolicy());//handler
  1. corePoolSize:线程池中的核心线程数
  2. maximumPoolSize: 线程池中的最大线程数
  3. keepAliveTime: 非核心线程在空闲状态下的存活时间。
  4. unit: keepAliveTime 的时间单位。
  5. workQueue: 任务队列,用于存储等待执行的任务。
  6. threadFactory: 线程工厂,用于创建新线程。可以设置线程名称、优先级等。默认情况下,使用 Executors.defaultThreadFactory() 创建线程。
  7. handler: 拒绝策略,当线程池和任务队列都已满时,用于处理新提交的任务。

线程池的工作原理

  1. 当调用 execute(Runnable command) 方法提交一个任务时,线程池会根据当前线程数和任务队列的状态决定如何处理这个任务。
  2. 如果当前线程数小于 corePoolSize,线程池会创建一个新的线程来执行这个任务,即使任务队列中有空闲位置。
  3. 如果当前线程数等于或大于 corePoolSize,线程池会尝试将任务放入任务队列 workQueue 中。如果任务队列已满,线程池会继续下一步。
  4. 如果当前线程数小于 maximumPoolSize,线程池会创建一个新的线程来执行这个任务,即使任务队列已满。如果当前线程数已经达到 maximumPoolSize,线程池会继续下一步。
  5. 如果当前线程数已经达到 maximumPoolSize 且任务队列已满,线程池会根据拒绝策略 RejectedExecutionHandler 处理新提交的任务。
  6. 线程池中的线程会从任务队列中取出任务并执行。每个线程在执行完一个任务后,会继续从任务队列中获取新的任务,直到任务队列为空。
  7. 如果线程池中的线程数超过 corePoolSize,并且某个线程在空闲时间超过 keepAliveTime,该线程会被终止。如果设置了 allowCoreThreadTimeOut(true),核心线程也会在空闲时间超过 keepAliveTime 后被终止。

拒绝策略:

常见的拒绝策略包括:

  • AbortPolicy:抛出 RejectedExecutionException 异常,线程池的默认拒绝策略。这种策略适用于一些比较重要的业务场景,开发者可以通过捕获异常及时处理。
  • CallerRunsPolicy:该策略会将任务提交给调用者所在的线程执行。这种方式适用于对时间要求不高,但不允许任务失败的场景。
  • DiscardPolicy:直接丢弃任务。这种策略适用于一些不重要的业务场景,如统计一些无关紧要的数据。
  • DiscardOldestPolicy:丢弃任务队列中最旧的任务,然后重新提交新任务。这种方式适用于那些执行时间较长的任务,可以避免长时间等待。

线程池常用的任务队列

任务队列类型类型特点适用场景
ArrayBlockingQueue有界阻塞队列基于数组实现,容量固定。适用于有界资源的场景,可以防止任务无限制地堆积。
LinkedBlockingQueue无界阻塞队列基于链表实现,容量可选(默认非常大)。适用于任务数量不确定且希望任务尽可能被处理的场景。
SynchronousQueue同步队列不存储元素,每个插入操作必须等待一个对应的移除操作。适用于直接移交任务的场景,任务提交者必须等待任务被处理。
PriorityBlockingQueue优先级阻塞队列基于优先级堆实现,可以按优先级顺序取出元素。适用于需要按优先级处理的场景,如任务调度。
DelayQueue延迟队列元素包含到期时间,只有到期时间前的元素会被取出。适用于需要延迟处理的场景,如定时任务。

四种内置的线程池

线程池类型特点适用场景
FixedThreadPool核心线程数和最大线程数大小一样,没有所谓的非核心线程的空闲时间。阻塞队列为无界队列LinkedBlockingQueue,可能会导致OOM适用于任务数量相对固定且执行周期长的任务。因为创建的都是核心线程,会一直存在,所以如果是短周期任务的话,比较浪费资源。任务并发量不确定的情况也最好不用。
CachedThreadPool核心线程数为0,最大线程数为Integer.MAX_VALUE(可能会因为无限创建线程,导致OOM),非核心线程的空闲时间是60s。阻塞队列为SynchronousQueue,即直接移交任务,不会缓存任务。适用于需要快速处理的任务,且任务数量不固定。因为创建的都是非核心线程,空闲时间比较长,所以适合处理比较紧急的任务。
ScheduledThreadPool最大线程数为Integer.MAX_VALUE(同样会因为无限创建线程,导致OOM),非核心线程的空闲时间是60s。阻塞队列为DelayedWorkQueue,即延迟队列,可以按任务执行时间排序。适用于需要周期性执行的任务
SingleThreadExecutor核心线程数为1,最大线程数为1,阻塞队列为SynchronousQueue,即直接移交任务,不会缓存任务。阻塞队列是无界队列LinkedBlockingQueue,可能会导致OOM。适用于需要串行执行的任务

总结四种线内置程池都相对比较极端,具体如下:

  • FixedThreadPool:固定线程池,只有核心线程,线程完全固定

  • CachedThreadPool:缓冲线程池,只有非核心线程,线程完全弹性

  • ScheduledThreadPool:调度线程池,实现延时任务和周期任务,非核心线程数没有上限

  • SingleThreadExecutor:单线程池,只能实现排队等待,完全没有并发

allowsCoreThreadTimeOut()

在线程池的使用中,有些场景并不是经常需要满负荷运行,即线程池中的核心线程数可能在大部分时间里都是空闲的。对于这样的应用场景,如果希望在没有任务时能够减少资源占用,就可以考虑使用 allowsCoreThreadTimeOut 参数。

allowsCoreThreadTimeOut 是 Java 中 ThreadPoolExecutor 类的一个方法,它允许你配置核心线程是否也能够在一段时间内无任务可做时被终止。默认情况下,核心线程不会因为空闲而被回收,即使设置了线程池的 keep-alive 时间。通过调用 allowsCoreThreadTimeOut(true),你可以改变这一行为,使得即使是核心线程,只要它们空闲的时间超过了设定的 keep-alive 时间,也可以被终止,从而释放资源。

这种方式非常适合那些负载不均匀的应用程序,可以在低负载时期节省系统资源,而在高负载时又可以迅速恢复到全功率运行。不过需要注意的是,频繁地创建和销毁线程可能会带来一定的性能开销,因此在使用 allowsCoreThreadTimeOut 时应当权衡利弊,并根据具体应用的需求进行适当的配置。

代码示例:

ThreadPoolExecutor pool = new ThreadPoolExecutor(4, 5, 5, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
pool.allowsCoreThreadTimeOut(true);