【Java】线程池(ExecutorService)相关的总结

483 阅读7分钟

【参考】

Java ExecutorService系列:

【本文内容】

  • 介绍了4种创建线程池的方式。
  • 线程池的构造参数。
  • 线程池的生命周期相关的方法。
  • 线程池提交Callacle task。

1. Executors的4种创建线程池的方式

  • FixedThreadPool
  • CachedThreadPool
  • ScheduledThreadPool
  • SingleThreadExecutor

1.1 newFixedThreadPool:创建固定大小的线程池

@Test
public void fixedThreadPool() {
    ExecutorService service = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 100; i ++) {
        service.execute(new Task());
    }
}

static class Task implements Runnable {
    public void run() {
        log.info("Thread Name: {}", Thread.currentThread().getName());
    }
}

线程池中的线程(下图的t0到t9),会做两件事:

  • 从queue中拿到下一个task任务。
  • 执行任务。

image.png

因为t0-t9可能会同时从queue中拿task,所以需要一个线程安全的queue来存储tasks,所以用的是BlockingQueue。

Java中的阻塞队列(BlockingQueue)与普通队列相比有一个重要的特点:在阻塞队列为空时会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。

【多少大小的线程数才比较合适?】

  • 对于计算密集型任务(高cpu消耗),线程数可以等于cpu数量。
int cpuCount = Runtime.getRuntime().availableProcessors();
  • 对于IO开销比较大的任务(如从DB中获取数据,http请求等),这种情况下可以将线程数扩大。 image.png 该公式参考于《Java高并发核心编程 卷2》。

1.2 CachedThreadPool

image.png

不同于上述的固定大小的线程池,CachedThreadPool没有固定大小的线程数,内部也没有BlockingQueue来存储task。内部有一个Synchronous queue,只能持有1个task,当我们提交了很多task后,CachedThreadPool会先拿一个task,然后去搜索线程池中已经创建的线程,看看是否有空闲的线程,如果有,则将task交给该线程运行,如果没有,则会在线程程中创建新的线程,并将该task交给该线程运行。

所以如果我们有100个tasks需要提交,极端情况下,每提交一个task就会创建一个线程(假如任务足够复杂),那么则可能创建了100个线程来运行这些tasks。基于此机制,所以在创建CachedThreadPool的时候无需传入线程数作为参数。

CachedThreadPool回收线程机制:当线程空闲时间超过60s(没有task在执行),那么就会kill(回收)该线程。

@Test
public void cachedThreadPool() {
    ExecutorService service = Executors.newCachedThreadPool();
    for (int i = 0; i < 100; i ++) {
        service.execute(new Task());
    }
}

static class Task implements Runnable {
    public void run() {
        log.info("Thread Name: {}", Thread.currentThread().getName());
    }
}

1.3 ScheduledThreadPool

image.png 比如我们提交的tasks需要延迟执行(多少分钟后执行),或者每间隔多久执行,这时候就需要用到ScheduledThreadPool。

ScheduledThreadPool内部有一个Deley Queue,用来存储tasks,存储的顺序是按tasks的执行顺序,而不是提交顺序。

@Test
public void scheduledThreadPool() throws InterruptedException {
    ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
    // 10秒后运行该task
    service.schedule(new Task(), 10, TimeUnit.SECONDS);
    
    // 每隔10秒运行该task,第一次在提交后15秒后运行。
    service.scheduleAtFixedRate(new Task(), 15, 10, TimeUnit.SECONDS);
    
    // 每当该task结束后隔10秒运行一次,第一次在提交后15秒后运行。
    service.scheduleWithFixedDelay(new Task(), 15, 10, TimeUnit.SECONDS);
}

static class Task implements Runnable {
    public void run() {
        log.info("Thread Name: {}", Thread.currentThread().getName());
    }
}

1.4 SingleThreadExecutor

image.png

SingleThreadExecutor有点像固定大小的线程池,只是线程数大小是1。

如果SingleThreadExecutor中的线程因为task的原因被kill,那么SingleThreadExecutor会重新创建一个线程来运行task。

SingleThreadExecutor可以用来执行固定顺序的tasks,比如图中的task1运行完毕后才会运行task2,依此类推。

1.5 关于并不推荐直接使用这4种线程池

具体请参考书《Java高并发核心编程 卷2》(book.douban.com/subject/354… 章节:1.6.11 Executors快捷创建线程池的潜在问题

2. ThreadPoolExecutor构造方法

我们上述通过ExecutorService service = Executors.newFixedThreadPool(10)创建的固定大小的线程池,其实质调用的是:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

即调用的是ThreadPoolExecutor构造方法:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

2.1 线程池的参数主要有

  • corePoolSize:核心线程数,即最小线程数、最初的线程数。核心线程数不会被回收,除非allowCoreThreadTimeOut(boolean)设为true。
  • maxPoolSize:最大线程数。
  • keepAliveTime + unit:用来指定当线程空闲时间超过多久时,该线程将被回收(killed)。
  • workQueue:内部用来存储tasks的BlockingQueue。
  • threadFactory:创建新的线程时使用的factory。
  • rejectedExecutionHandler:当线程提交被拒绝时的策略(回调机制)。

关于线程数的图示: image.png

核心线程数即初始的线程数,不一定等于目前线程池中的线程数,目前的线程数还要根据线程池的种类以及线程空闲后存活多久来决定的。

以下是第1章介绍的4种线程池的参数情况:

参数FixThreadPoolCachedThreadPoolScheduledThreadPoolSingleThreadExecutor
corePoolSize自定义0自定义1
maxPoolSize和corePoolSize相同Integer.MAX_VALUEInteger.MAX_VALUE1
keepAliveTime0秒60秒60秒0秒

2.2 Blocking Queue

我们在第1章的时候有过介绍,线程池内部存储tasks的容器为Blocking Queue,即任务阻塞队列,在阻塞队列为空时会阻塞当前线程的元素获取操作。

这里列了各个线程池内部使用的Blocking Queue:

PoolBlocking Queue使用的原因
FixThreadPoolLinkedBlockingQueue线程数是有限的,所以需要无界的队列来存储所有的tasks。无界的意思指长度为长度为Integer.MAX_VALUE。ps.因为队列永远不会被填满,所以不会创建新的线程。
CachedThreadPoolSynchronousQueue线程数可以是无限的,但队列中只需要hold一个队列,所以SynchronousQueue只需要存储一个元素。
ScheduledThreadPoolDelayedWorkQueue可以处理延迟的一种特殊的队列。
SingleThreadExecutorLinkedBlockingQueue同FixThreadPool
自定义线程的选择ArrayBlockingQueue有界队列,如果队列满了(存放的tasks满了),那么新的线程将被创建(前提是线程数不会超过maxPoolSize,否则就实行拒绝策略)。

2.3 拒绝策略

任务被拒绝有两种情况:

  • (1)线程池已经被关闭。
  • (2)工作队列已满且maximumPoolSize已满。

具体的实现有:

Policy描述
AbortPolicy新提交tasks会抛出RejectedExecutionException(是Runtime exception)。
DiscardPolicy新提交tasks会被忽略。
DiscardOldestPolicy新提交tasks时会丢掉最老的task(也就是队尾的),然后将新的task放入queue中。
CallerRunsPolicy新提交tasks会使用提交task当前的线程去执行该task,不会使用线程池中的线程去执行该新task。具体来讲,如果main thread负责提交tasks,当源源不断的tasks被提交,达到了maxPoolSize,于是就会触发拒绝策略,当策略是CallerRunsPolicy时,线程池会让main thread自己来执行该新task。这样main thread在执行task的同时,也会减慢往线程池提交更多新tasks的速度。
自定义拒绝策略实现RejectedExecutionHandler接口的rejectedExecution方法即可。

2.4 关于线程池的参数,总结

总结

3. 线程池的生命周期

  • service.shutdown():关闭线程池,该方法并不会马上关闭线程池,如果线程池的Blocking Queue中依旧有tasks,会继续执行tasks。该方法被调用后,线程池将不再接收新的tasks(如果继续提交新task,则会触发拒绝策略)。
  • service.isShutdown():该方法可以返回是否已经被执行过上述的shutdown()方法。如果返回true,则表示线程池已经知道要准备关闭了。
  • service.isTerminated():该方法用来检查线程池中Blocking Queue中所有的tasks已经执行结束了,如果是则返回true。
  • service.awaitTermination(10, TimeUnit.SECONDS):如果线程池中有tasks,那么阻塞等待tasks执行,直到超出设定的时间(这里是10秒)。
  • service.shutdownNow():立即关闭线程池,并返回线程池Blocking Queue中的tasks(即未被执行的tasks)。

4. 线程池提交Callable的task

线程池可以通过execute()来提交task,该task可以是Thread实例,也可以是继承了Runnable接口的实例。

线程池还有一个方法叫submit(),该方法可以接收Runnable的实例,还可以接收Callable的实例(即可以有返回值的task)。

service.submit()提交了一个Callable的task后,返回Future类,这个Future可以认为是拿结果的占位符或是吃饭点餐时的小票,我们拿到小票后可以继续干别的事情(如去隔壁店里买饮料),等我们真正想要拿结果的时候,可以用future.get()方法,如果这时候task还没有执行完毕,那么需要阻塞式的等待结果。

@Test
public void futureTask() {
    ExecutorService service = Executors.newFixedThreadPool(10);
    Future<Integer> future = service.submit(new Task());

    // 在此做一些别的事情

    try {
        // 回过头来拿结果
        int result = future.get(); // 阻塞
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } catch (ExecutionException e) {
        throw new RuntimeException(e);
    }
}

static class Task implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(3000L);
        return new Random().nextInt();
    }
}

也可以批量提交,将提交的Future先保存到一个list中,等到想要拿结果的时候再去拿:

    // 批量提交
    List<Future<Integer>> futureList = new ArrayList<>();
    for (int i = 0; i < 100; i ++) {
        futureList.add(service.submit(new Task()));
    }

    // 在此做一些别的事情
    
    // 拿结果
    for (int i = 0; i < 100; i ++) {
        Future<Integer> future = futureList.get(i);
        try {
            int result = future.get(); // 阻塞
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

往线程池中提交了一个新的Callable task时,线程池会立马返回一个Future实例(即图中的placeholder占位符),当该task真正结束时,线程池将结果更新到刚刚返回给main thread的placeholder中。

如果task没有执行完毕,main thread通过future.get()方法想要拿到结果时,则需要等待该task运行完毕(阻塞式等待)。 image.png

在上述批量提交的代码中,我们将结果保存在一个list中,那么有可能出现的问题是,task1的结果可能还没有出来,但因为是阻塞式等待,这会影响拿到后续tasks结果的速度。 image.png

我们可以使用方法future.get(1, TimeUnit.SECOND)有限的进行等待,即等待1秒,要是没有返回结果,则报TimeoutException。

try {
    int result = future.get(); // 阻塞
} catch (InterruptedException e) {
    throw new RuntimeException(e);
} catch (ExecutionException e) {
    System.out.println("超时,task运行还没有结束。");
    throw new RuntimeException(e);
}

其它有用的方法:

  • future.cancel(false):取消task,如果该task还处于线程池的Blocking Queue中(等待被执行),这时候通过该方法想要取消task,该task可以立马被取消。但是如果该task正在被线程池中的线程执行中,那么则需要根据传入的参数(boolean)来决定是否需要中断该task,传入mayInterruptIfRunning=false,则表示既然在执行了就不要中断该运行中的task了。
  • future.isCancelled():返回true表示该task已经取消了。
  • future.isDown():返回true表示该task已经执行完成了。