多线程编程

432 阅读7分钟

前言

本文主要介绍在Java8版本下,多线程编程的方法,在阅读之前,需要对JUC有一定了解,包括但不限于:线程的生命周期,线程的六种状态,多线程的上下文切换,线程的死锁,线程池基础知识等。

1、线程池

线程池适用于解决接口中,某些可以异步执行的业务,但如果业务非常的重要,或者并发特别高,建议使用MQ去异步处理,因为MQ比线程池更可控,功能更强大,比如,MQ可以持久化等等。就像铁锹和挖掘机,当活特别复杂,要求特别高的时候,就要考虑花钱用挖掘机了,当然用铁锹也有它的好处,成本比较低,所以在实际开发中,两者要搭配使用。

下图是线程池接收到线程后,处理的流程: image.png

  1. 线程执行完任务后不会直接关闭,接着去执行等待队列中的任务,如果没有任务可执行,则空闲配置keepAlive的时间,如果到了时间则关闭;
  2. 这个流程一定要记住,可以帮助你排查线上问题。

1.1、定义一个线程池

public class DemoThreadPool {
    /**
     * 线程名称
     */
    private static final String THREAD_NAME_PREFIX = "demo-thread-pool-";
    /**
     * 线程池维护核心线程数
     * cpu * 2: Runtime.getRuntime().availableProcessors() * 2
     */
    private static final int CORE_POOL_SIZE = 5;
    /**
     * 线程池最大线程数
     */
    private static final int MAX_POOL_SIZE = CORE_POOL_SIZE * 2;
    /**
     * 最大空闲时间,一分钟
     */
    private static final long KEEP_ALIVE_TIME = TimeUnit.MINUTES.toSeconds(1);
    /**
     * 缓冲队列
     */
    private static final int BLOCKING_QUEUE_SIZE = 10 * 1024;
    /**
     * 线程编号
     */
    private static AtomicInteger num = new AtomicInteger(0);

    public static ThreadPoolExecutor pool = new ThreadPoolExecutor(
            CORE_POOL_SIZE,
            MAX_POOL_SIZE,
            KEEP_ALIVE_TIME,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(BLOCKING_QUEUE_SIZE),
            (r) -> {
                Thread t = new Thread(r);
                t.setName(THREAD_NAME_PREFIX + num.incrementAndGet());
                t.setDaemon(true);
                return t;
            },
            new ThreadPoolExecutor.CallerRunsPolicy());

    static {
        pool.prestartAllCoreThreads();
    }

    public static void execute(Runnable runnable) {
        pool.execute(runnable);
    }

    public static <T> Future<T> submit(Callable<T> callable) {
        return pool.submit(callable);
    }

    public static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
        }
    }
}

1.2、线程池队列

在Java中,线程池一般有三种工作队列:

  1. 直接提交队列(SynchronousQueue):这种队列中不会保存任务,而是直接提交任务给线程处理。如果当前没有线程可用,则新建一个线程处理任务,否则将任务添加到一个可用线程的工作队列。这种队列适用于提交处理时间较短的任务。

  2. 有界队列(ArrayBlockingQueue):这种队列中有一个指定的容量,当任务数达到容量上限时,新提交的任务将会被阻塞,直到有空闲线程可用或有任务被执行完成。

  3. 无界队列(LinkedBlockingQueue):这种队列中没有指定容量上限,如果所有的线程都在执行任务并且队列已满,新提交的任务就会在队列中等待。这种队列适用于处理时间较长的任务。

1.3、拒绝策略

拒绝策略有以下四种:

  1. AbortPolicy 策略(默认):会直接抛出RejectedExecutionException 的 RuntimeException,程序可以采用重试或放弃。
  2. DiscardPolicy策略:会直接默默丢失,不给你任何提示,不建议使用,容易莫名其妙丢任务。
  3. DiscardOldestPolicy策略:丢弃任务队列中等待事件最长的,即最老的任务,和上一个区别是上一个丢弃的是新提交的任务,这个丢弃的是最老的任务。丢弃后就可以腾出一个队列的空位存放任务。
  4. CallerRunsPolicy策略:谁提交任务谁来执行这个任务,即将任务执行放在提交的线程里面,减缓了线程的提交速度,相当于负反馈。在提交任务线程执行任务期间,线程池又可以执行完部分任务,从而腾出空间来。

1.4、注意事项

  1. 使用 ThreadPoolExecutor 的构造函数声明线程池,别的不易使用,使用不当可能OOM;
  2. 建议不同类别的业务用不同的线程池,父子任务不要使用同一个线程池;
  3. 给线程池命名,查问题方便;
  4. 正确配置线程池参数:核心线程数,CPU 密集型任务(N+1),I/O 密集型任务(2N);拒绝策略,重要就选CallerRunsPolicy,否者就丢弃。

2、使用线程池异步处理

2.1、第一种

DemoThreadPool.execute(()->{

});

2.2、第二种

使用CompletableFuture,可以拿到异步处理后的结果,还可以在发生异常后进行处理。推荐这种方式。

CompletableFuture.runAsync(() -> {
    //异步执行的业务
}, DemoThreadPool.pool).exceptionally((e) -> {
    log.error("demo error", e);
    return null;
});

2.3、线程池监控

监测线程池运行状态,比如 SpringBoot 中的 Actuator 组件;

2.4、动态修改线程池参数

阅读# Java线程池实现原理及其在美团业务中的实践

效果图: image.png 推荐使用开源项目: dynamic-tp

3、CompletableFuture

在2.2中,使用CompletableFuture函数式编程,进行异步多线程执行时,可以运用CompletableFuture自带的一些方法,让你的代码可读性更高,代码逻辑处理起来更加方便。下面就将介绍它的一些常用方法。

3.1、创建方式

  1. new CompletableFuture<>();
  2. CompletableFuture.runAsync();
  3. CompletableFuture.supplyAsync();

推荐第二种或第三种,第二种不接收计算的结果,第三种接收。现实开发中使用第二种和第三种,建议传入自定义线程池去执行,像2.2中那样,传入DemoThreadPool,即使用jdk中的这个静态方法:

public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor) {
    return asyncRunStage(screenExecutor(executor), runnable);
}

禁止使用:

public static CompletableFuture<Void> runAsync(Runnable runnable) {
    return asyncRunStage(asyncPool, runnable);
}

因为这样,会使用ForkJoinPool的commonPool,或者new一个默认线程池,即使用不是我们自己管理的线程池,线上有问题时,无法定位问题,且默认的线程池的线程大小和你的业务也不适用。

3.1、获取结果

获取到异步计算的结果,对其进行进一步的处理,比较常用的方法有下面几个:

  • thenApply()方法接受一个Function实例,用它来处理结果;
  • thenAccept()方法的参数是Consumer<? super T> ,顾名思义,Consumer属于消费型接口,它可以接收 1 个输入对象然后进行“消费”;
  • thenRun()的方法是的参数是Runnable;
  • whenComplete()的方法的参数是BiConsumer<? super T, ? super Throwable>。相对于Consumer,BiConsumer可以接收 2 个输入对象然后进行“消费”。

3.2、异常处理

可以通过handle()方法来处理任务执行过程中可能出现的抛出异常的情况。你还可以通过exceptionally()方法来处理异常情况,即2.2中所示。如果你想让 CompletableFuture的结果就是异常的话,可以使用 completeExceptionally()方法为其赋值。

3.3、组合CompletableFuture

3.3.1、thenCompose()

thenCompose()按顺序链接两个CompletableFuture对象。在实际开发中,这个方法还是非常有用的。比如说,我们先要获取用户信息然后再用用户信息去做其他事情。示例:

CompletableFuture<String> future
        = CompletableFuture.supplyAsync(() -> "hello!")
        .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "world!"));
assertEquals("hello!world!", future.get());

3.3.2、thenCombine()

和thenCompose()方法类似的, thenCombine()同样可以组合两个CompletableFuture对象。示例:

CompletableFuture<String> completableFuture
        = CompletableFuture.supplyAsync(() -> "hello!")
        .thenCombine(CompletableFuture.supplyAsync(
                () -> "world!"), (s1, s2) -> s1 + s2)
        .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "nice!"));
assertEquals("hello!world!nice!", completableFuture.get());

3.3.3、它俩区别

  • thenCompose()可以两个CompletableFuture对象,并将前一个任务的返回结果作为下一个任务的参数,它们之间存在着先后顺序。
  • thenCombine()会在两个任务都执行完成后,把两个任务的结果合并。两个任务是并行执行的,它们之间并没有先后依赖顺序。

3.4、并行运行多个CompletableFuture

通过CompletableFuture的静态方法allOf()和anyOf()。

3.4.1、allOf()

allOf()方法会等到所有的CompletableFuture都运行完成之后再返回 。实际项目中,我们经常需要并行运行多个互不相关的任务,这些任务之间没有依赖关系,可以互相独立地运行。

比说我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。像这种情况我们就可以使用并行运行多个CompletableFuture来处理。

注意:allOf()可以帮你优化代码执行时间,俗称并行执行,必须掌握。

3.4.2、anyOf()

方法不会等待所有的 CompletableFuture 都运行完成之后再返回,只要有一个执行完成即可!

4、其它

学会以下的内容,让你在高并发编程时,随心应手。

  1. Atomic 原子类,进行原子操作;
  2. ThreadLocal创建线程副本;
  3. CountDownLatch,起多个线程同时处理;
  4. CyclicBarrier(循环栅栏),比CountDownLatch强大;
  5. ReentrantLock和synchronize使用;
  6. 并发容器,掌握ConcurrentHashMap、CopyOnWriteArrayList、了解ConcurrentLinkedQueue、BlockingQueue、ConcurrentSkipListMap;
  7. 了解AQS原理和AQS同步组件。