Java并发编程:线程池

1. 概述

1.1 线程池的特点

线程池 (Thread Pool) 是一种基于池化思想帮助我们管理线程而获取并发性的工具,经常出现在多线程服务器中,如 MySQL。线程池的实现思路:提前创建好多个线程,让这些线程处于就绪状态来提高系统响应速度,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁,实现重复利用。

内存池 (Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
连接池 (Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
实例池 (Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。(线程复用
  • 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会因为线程的不合理分布,导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。(控制最大并发数
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池 ScheduledThreadPoolExecutor 允许任务延期执行或定期执行。

1.2 异步回调

  • 同步和异步通常用来形容一次方法调用。
  • 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
  • 异步方法调用一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另一个线程中“真实”地执行。整个过程,不会阻碍调用者的工作。对于调用者来说,异步调用似乎是一瞬间完成的。如果异步调用需要返回结果,那么当这个异步调用真实完成时,会通知调用者。
package com.java.forkjoinpool;

import java.util.concurrent.CompletableFuture;

/**
 * @author rrqstart
 * @Description 异步回调
 * public class CompletableFuture implements Future, CompletionStage {//......}
 */
public class CompletableFutureTest {
    public static void main(String[] args) throws Exception {
        //异步调用,无返回值,开启一个新线程来执行任务
        //public static CompletableFuture runAsync(Runnable runnable)
        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName() + ":无返回值。");
        });
        Void v = completableFuture1.get();
        System.out.println(v); //null

        System.out.println("----------------------------------------");

        //异步回调,有返回值,开启一个新线程来执行任务
        //public static  CompletableFuture supplyAsync(Supplier supplier)
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + ":有返回值。");
//            int i = 10 / 0;
            return 1024;
        });
        Integer i = completableFuture2.whenComplete((t, u) -> {
            System.out.println("t = " + t);
            System.out.println("u = " + u);
        }).exceptionally(f -> {
            System.out.println("exception : " + f.getMessage());
            return 404;
        }).get();
        System.out.println(i);
    }
}
//int i = 10 / 0;被注释掉时程序的输出结果:
ForkJoinPool.commonPool-worker-9:无返回值。
null
----------------------------------------
ForkJoinPool.commonPool-worker-9:有返回值。
t = 1024
u = null
1024
//int i = 10 / 0;没有被注释掉时程序的输出结果:
ForkJoinPool.commonPool-worker-9:无返回值。
null
----------------------------------------
ForkJoinPool.commonPool-worker-9:有返回值。
t = null
u = java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
exception : java.lang.ArithmeticException: / by zero
404

2. Executor框架

2.1 Executor框架简介

  • 在 Java 中,使用线程来异步的执行任务。Java 的线程既是工作单元,也是执行机制。从 JDK1.5 开始,把工作单元与执行机制分离开来,工作单元包括 Runnable 和 Callable,执行机制由 Executor 框架提供。
  • Executor 框架的两级调度模型
    • 在 HotSpot VM 的线程模型中,Java 线程被一对一映射为本地操作系统的线程。当 Java 线程启动时会创建一个本地操作系统线程;当该 Java 线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。
    • 在上层,Java 多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor 框架)将这些任务映射为固定数量的线程;在底层,操作系统内核(OS Kernel)将这些线程映射到硬件处理器上。
  • Executor 框架的结构
    • 任务:包括被执行任务需要实现的接口 Runnable 和 Callable。
    • 任务的执行:包括任务执行机制的核心接口 Executor,以及继承自 Executor 的 ExecutorService 接口。
    • 异步计算的结果:包括接口 Future 和实现 Future 的 FutureTask 类。
    • ExecutorService 是真正的线程池接口,常见子类是 ThreadPoolExecutor。

Java并发编程:线程池_第1张图片

2.2 Executors源码分析

package java.util.concurrent;
//Since:JDK1.5
public class Executors {
	//1.创建一个只有一个线程的线程池:一个任务一个任务的执行,适用于需要保证顺序的执行各个任务。
	public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
    }
    
    //2.创建一个固定线程数的可重用的线程池:执行长期任务性能好,适合严格限制线程数的场景,如:负载比较重的服务器。
    public static ExecutorService newFixedThreadPool(int nThreads) {
    	//使用无界队列LinkedBlockingQueue(队列容量为:Integer.MAX_VALUE)作为线程池的工作队列
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    }
    
    //3.创建一个线程数可伸缩的线程池:适用于执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重启它们。可扩容!
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    }
	/**
	 * ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。
	 * 表示在给定的延迟之后执行任务,或者定期执行任务。
	 * ScheduledThreadPoolExecutor的功能与Timer类似,但其功能更强大、更灵活。
	 * Timer对应的是单个后台线程,ScheduledThreadPoolExecutor可以对应1个或多个后台线程。
	 */
	//4.创建固定数量线程的线程池:适合多个后台线程执行周期任务,同时为了满足资源管理的需求而限制后台线程数量的场景。
	public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
	
	//5.创建单个线程的线程池:适合需要单个后台线程执行周期任务,同时需要保证顺序的执行各个任务的应用场景。
	public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService(new ScheduledThreadPoolExecutor(1));
    }
}

在阿里巴巴《Java开发手册》中提到,使用 Executors 创建线程池可能会导致 OOM。比如:newFixedThreadPoolnewSingleThreadExecutor中创建 LinkedBlockingQueue 时,默认并未指定容量,此时 LinkedBlockingQueue 就是一个无边界队列,最大长度为 Integer.MAX_VALUE,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。newCachedThreadPoolnewScheduledThreadPool这两种方式创建的最大线程数可能是 Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致 OOM。避免使用 Executors 创建线程池,主要是避免使用其中的默认实现,我们可以自己直接调用 ThreadPoolExecutor 的构造函数来创建线程池。除了自己定义 ThreadPoolExecutor 外,还可以使用开源类库,如 apache、guava 等。

2.3 Callable与FutureTask

2.3.1 Callable

package java.util.concurrent; //since JDK1.5

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

2.3.2 FutureTask

  • Future接口和FutureTask类代表异步计算的结果。
  • 可以把FutureTask交给Executor执行,也可以通过ExecutorService.submit(…)方法返回一个FutureTask,然后执行FutureTask.get()方法或FutureTask.canael(…)方法。也可以单独使用FutureTask:当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行,此时可以使用FutureTask。
  • FutureTask是基于AbstractQueuedSynchronizer(AQS)实现的。
  • 基于AQS实现的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch、FutureTask。

FutureTask类的部分源码(since JDK1.5)

package java.util.concurrent;

//RunnableFuture接口的父接口:Future、Runnable
//FutureTask类实现的接口:RunnableFuture、Future、Runnable
public class FutureTask<V> implements RunnableFuture<V> {
	//构造器1
	public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW; //ensure visibility of callable
    }
    
    //构造器2
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW; //ensure visibility of callable
    }
	
	/**
	 * 根据run()方法被执行的时机,FutureTask可处于三种状态:
	 *(1)未启动:创建了FutureTask但还未执行run()方法时FutureTask处于该状态。
	 *(2)已启动:run()方法被执行时。
	 *(3)已完成:run()方法执行完后正常结束,或被cancel()方法取消,或run()方法抛出异常而异常结束时。
	 */
	public void run() { //...... }
	
	/**
	 * 当FutureTask处于未启动或已启动状态时,执行get()方法将导致调用线程阻塞。
	 * 当FutureTask处于已完成状态时,执行get()方法将导致调用线程立即返回结果或抛出异常。
	 */
	public V get() throws InterruptedException, ExecutionException { //...... }
	
	/**
	 * 当FutureTask处于未启动状态时,执行cancel(...)方法将导致此任务永远不会被执行。
	 * 当FutureTask处于已启动状态时,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务。
	 * 当FutureTask处于已启动状态时,执行cancel(false)方法将不会对正在执行此任务的线程产生影响。
	 * 当FutureTask处于已完成状态时,执行cancel(...)方法将返回false。
	 */
	public boolean cancel(boolean mayInterruptIfRunning) { //...... }
}

2.3.3 案例分析

package com.java.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

/**
 * @author rrqstart
 * 创建线程的方式:
 * * (1)继承Thread类
 * * (2)实现Runnable接口:无泛型、重写run方法、无返回值、不可抛异常
 * * (3)实现Callable接口:有泛型、重写call方法、有返回值、可抛异常
 * 

* FutureTask可用于异步获取执行结果或取消执行任务的场景。通过get()方法可以异步获取执行结果。 * 不论FutureTask调用多少次run()或者call()方法,它都能确保只执行一次Runable或Callable任务。 * 因此,FutureTask非常适合用于耗时高并发的计算,另外可以通过cancel()方法取消执行任务。 */ public class ThreadWithCallable { public static void main(String[] args) throws ExecutionException, InterruptedException { MyThread thread = new MyThread(); FutureTask<Integer> futureTask = new FutureTask<Integer>(thread); new Thread(futureTask, "A").start(); //在这个线程的任务耗时约2秒 //如果再添加一个线程,结果还是打印一条结果 new Thread(futureTask, "B").start(); //主线程可以完成自己的任务后,去获取其他线程的结果 System.out.println(Thread.currentThread().getName() + "-->计算完成"); //这个get方法,可能会产生阻塞,应该放在代码的最后,或者使用异步通信来处理 System.out.println(futureTask.get()); /* * 执行结果: * main-->计算完成 * ------ come in call------ * 1024 */ } } //泛型带什么类型,call方法就返回什么类型 class MyThread implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("------ come in call------"); TimeUnit.SECONDS.sleep(2); return 1024; } }

3. 线程池的使用

3.1 线程池的创建

3.1.1 ThreadPoolExecutor源码分析

package java.util.concurrent;
//Since:JDK1.5
public class ThreadPoolExecutor extends AbstractExecutorService {
	// runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;
    
	//The context to be used when executing the finalizer, or null.
    private final AccessControlContext acc;
    
    private volatile int corePoolSize; //核心线程池的大小

    private volatile int maximumPoolSize; //最大线程池的大小

    private final BlockingQueue<Runnable> workQueue; //用来暂存任务的工作队列

    private volatile long keepAliveTime;
    
    private volatile ThreadFactory threadFactory;

    private volatile RejectedExecutionHandler handler;

	//默认的拒绝策略:AbortPolicy
    private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

	//预定义的4种拒绝策略的静态内部类
    public static class CallerRunsPolicy implements RejectedExecutionHandler {......}
    public static class AbortPolicy implements RejectedExecutionHandler {......}
    public static class DiscardPolicy implements RejectedExecutionHandler {......}
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {......}

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

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

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

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ? null : AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
}

3.1.2 7大参数

  • corePoolSize:线程池中的常驻核心线程数,是线程池的基本大小。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池的基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
    • 核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干(铁饭碗),而非核心线程如果长时间的闲置,就会被销毁(临时工)。
  • maximumPoolSize:线程池中能够容纳同时执行的最大线程数,也就是线程池允许创建的最大线程数,此值必须大于等于1。
    • 该值等于核心线程数量 + 非核心线程数量。
  • keepAliveTime:非核心线程闲置超时时长。当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余的线程会被销毁,直到只剩下corePoolSize个线程为止。
    • 非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则也会作用于核心线程。
  • unit:keepAliveTime的单位。
  • workQueue:任务队列,该阻塞队列中维护着被提交但等待执行的Runnable任务对象。
  • threadFactory:创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否是守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。
    • 通过Google工具包可以设置线程池里的线程名:new ThreadFactoryBuilder().setNameFormat("general-detail-batch-%d").build()
  • handler:拒绝策略。表示当队列和线程池都满了,即线程池处于饱和状态时,或者线程池已经关闭了的时候,如何来拒绝请求执行的任务的策略。

3.1.3 4大拒绝策略

  • 等待队列已经满了,再也塞不下新任务了,同时,线程池中的最大线程数也达到了,无法继续为新任务服务。此时就需要拒绝策略机制合理的处理这个问题。
  • AbortPolicy:默认的拒绝策略。直接抛出RejectedExecutionException异常阻止系统正常运行。
  • CallerRunsPolicy:“调用者运行”的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
  • DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种策略。

3.2 向线程池提交任务

  • public void execute(Runnable command):用于提交不需要返回值的任务,所以该方法无法判断任务是否被线程池执行成功。
  • public Future submit(Runnable task):用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个对象可以判断任务是否执行成功,并且可以通过这个对象的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout, TimeUnit unit)方法会阻塞当前线程一段时间后立即返回,这时候可能任务没有执行完。
  • public Future submit(Runnable task, T result)
  • public Future submit(Callable task)

3.3 关闭线程池

  • public void shutdown():遍历线程池中的工作线程,然后逐个调用线程的interrupt()方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdown()方法只是将线程池的状态设置成SHUTDOWN,然后中断所有没有正在执行任务的线程。
  • public List shutdownNow():遍历线程池中的工作线程,然后逐个调用线程的interrupt()方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow()方法首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
  • public boolean isShutdown():只要调用了shutdown()或shutdownNow()中的任意一个,isShutdown()方法就返回true。
  • public boolean isTerminated():当所有任务都已关闭后,才表示线程池关闭成功,此时调用isTerminated()方法会返回true。

3.4 合理地配置线程池

  • CPU密集型任务:maximumPoolSize = Ncpu + 1(配置尽可能小的线程池)
  • IO密集型任务:maximumPoolSize = Ncpu * Ucpu * (1 + W / C)maximumPoolSize = 2 * Ncpu(由于IO密集型任务的线程并不是一直在执行任务,CPU的利用率不高,应配置尽可能大的线程池)
    • Ncpu = CPU的数量
    • Ucpu = 目标CPU的使用率(0 ≤ Ucpu ≤ 1)
    • W / C = 等待时间与计算时间的比率(当处理的任务为计算密集型任务时等待时间W为0)

获取当前设备的逻辑处理器个数Ncpu:

  • int count = Runtime.getRuntime().availableProcessors();

3.5 线程池的状态

线程池本身有一个调度线程,这个线程就是用于管理布控整个线程池里的各种任务和事务,例如创建线程、销毁线程、任务队列管理、线程队列管理等等,故线程池也有自己的状态。ThreadPoolExecutor 类中使用了一些 private static final int 型常量来表示线程池的状态。

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

Java并发编程:线程池_第2张图片

  • RUNNING:线程池创建后处于 RUNNING 状态(运行状态),处于此状态的线程池,接收新任务,持续处理任务队列里的任务。
  • SHUTDOWN:调用 shutdown() 方法后处于 SHUTDOWN 状态,线程池不能接受新的任务,但要处理任务队列里的任务,清除一些空闲 worker,会等待阻塞队列的任务完成。
  • STOP:调用 shutdownNow() 方法后处于 STOP 状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,poolsize=0,阻塞队列的 size=0。
  • TIDYING:表示线程池正在停止运作,中止所有任务,销毁所有工作线程。当所有的任务已终止,ctl 记录的”任务数量”为 0,线程池会变为 TIDYING 状态。接着会执行 terminated() 函数。
    • ThreadPoolExecutor 中有一个控制状态的属性叫 ctl,它是一个 AtomicInteger 类型的变量。线程池状态就是通过 AtomicInteger 类型的成员变量 ctl 来获取的。
    • 获取的 ctl 值传入 runStateOf() 方法,与 ~CAPACITY 位与运算(CAPACITY 是低 29 位全 1 的 int 变量)。
    • ~CAPACITY 在这里相当于掩码,用来获取 ctl 的高 3 位,表示线程池状态;而另外的低 29 位用于表示工作线程数。
  • TERMINATED:表示线程池已经停止运作,所有工作线程已被销毁,所有任务已被清空或执行完毕。线程池处在 TIDYING 状态时,执行完成 terminated() 方法之后,就会由 TIDYING → TERMINATED, 线程池被设置为 TERMINATED 状态。

4. 线程池的实现原理

Java并发编程:线程池_第3张图片
Java并发编程:线程池_第4张图片
Java并发编程:线程池_第5张图片

  • ThreadPoolExecutor 之所以采取上述步骤,是为了在执行 execute() 方法时,尽可能地避免获取全局锁(2.1和2.3两步在执行时需要获取全局锁)。当正在运行地线程数大于等于 corePoolSize 以后,几乎所有的 execute() 方法调用都会执行步骤2.2,而步骤2.2无需获取全局锁。

5. ThreadPoolExecutor线程复用原理

一个线程在创建的时候会指定一个线程任务,当执行完这个线程任务之后,线程自动销毁。但是线程池却可以复用线程,即一个线程执行完线程任务后不销毁,继续执行另外的线程任务。那么,线程池是如何做到线程复用的呢?ThreadPoolExecutor 在创建线程时,会将线程封装成工作线程 Worker,并放入工作线程组中,然后这个 Worker 反复从阻塞队列中拿任务去执行。

6. 案例分析

package com.java.threadpool;

import java.util.concurrent.*;

/**
 * @author rrqstart
 * Executors:3大方法
 * ThreadPoolExecutor:7大参数
 * RejectedExecutionHandler:4大拒绝策略
 */
public class ThreadPoolTest {
    public static void main(String[] args) {
        /*
         * 线程池不允许使用 Executors 去创建!!!
         * 而是通过 ThreadPoolExecutor 的方式创建线程池。这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险。
         * Executors 返回的线程池对象的弊端如下:
         * (1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
         * (2) CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
         */
        //1.创建线程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                3L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        //new LinkedBlockingQueue() 等价于 new LinkedBlockingQueue(Integer.MAX_VALUE)

        try {
            for (int i = 1; i <= 10; i++) {
                //2.向线程池提交任务
                threadPool.execute(() -> {
                	System.out.println(Thread.currentThread().getName() + " --> 办理业务!");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //3. 关闭线程池
            threadPool.shutdown();
        }
    }
}
//采取中止策略AbortPolicy的执行结果:
pool-1-thread-1 --> 办理业务!
pool-1-thread-5 --> 办理业务!
pool-1-thread-4 --> 办理业务!
pool-1-thread-3 --> 办理业务!
pool-1-thread-2 --> 办理业务!
pool-1-thread-4 --> 办理业务!
pool-1-thread-5 --> 办理业务!
pool-1-thread-1 --> 办理业务!
java.util.concurrent.RejectedExecutionException: Task com.java.threadpool.ThreadPoolTest$$Lambda$1/1023892928@4dd8dc3 rejected from java.util.concurrent.ThreadPoolExecutor@6d03e736[Running, pool size = 5, active threads = 4, queued tasks = 0, completed tasks = 4]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at com.java.threadpool.ThreadPoolTest.main(ThreadPoolTest.java:32)

Process finished with exit code 0
//采取调用者运行策略CallerRunsPolicy的执行结果:
pool-1-thread-1 --> 办理业务!
pool-1-thread-5 --> 办理业务!
pool-1-thread-1 --> 办理业务!
pool-1-thread-4 --> 办理业务!
pool-1-thread-3 --> 办理业务!
pool-1-thread-2 --> 办理业务!
main --> 办理业务!
pool-1-thread-1 --> 办理业务!
pool-1-thread-5 --> 办理业务!
pool-1-thread-1 --> 办理业务!
//采取丢弃策略DiscardPolicy的执行结果:
pool-1-thread-2 --> 办理业务!
pool-1-thread-5 --> 办理业务!
pool-1-thread-3 --> 办理业务!
pool-1-thread-3 --> 办理业务!
pool-1-thread-4 --> 办理业务!
pool-1-thread-1 --> 办理业务!
pool-1-thread-5 --> 办理业务!
pool-1-thread-2 --> 办理业务!

7. Tomcat线程池分析

7.1 Tomcat请求处理过程

Java并发编程:线程池_第6张图片
Tomcat 的整体架构包含连接器和容器两大部分,其中连接器负责与外部通信,容器负责内部逻辑处理。在连接器中:

  1. 使用 ProtocolHandler 接口来封装I/O模型和应用层协议的差异,其中I/O模型可以选择非阻塞I/O、异步I/O或APR,应用层协议可以选择HTTP、HTTPS或AJP。ProtocolHandler 将I/O模型和应用层协议进行组合,让 EndPoint 只负责字节流的收发,Processor 负责将字节流解析为 Tomcat Request/Response 对象,实现功能模块的高内聚和低耦合,ProtocolHandler 接口继承关系如下图示。
  2. 通过适配器 Adapter 将 Tomcat Request 对象转换为标准的 ServletRequest 对象。

Java并发编程:线程池_第7张图片
Tomcat为了实现请求的快速响应,使用线程池来提高请求的处理能力。下面我们以HTTP非阻塞I/O为例对Tomcat线程池进行简要的分析。

7.2 Tomcat线程池创建

Java并发编程:线程池_第8张图片
在Tomcat中,通过AbstractEndpoint类提供底层的网络I/O的处理,若用户没有配置自定义公共线程池,则AbstractEndpoint通过createExecutor方法来创建Tomcat默认线程池。核心部分代码如下:

public void createExecutor() {
	internalExecutor = true;
	TaskQueue taskqueue = new TaskQueue();
	TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
	executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
	taskqueue.setParent( (ThreadPoolExecutor) executor);
}

其中 TaskQueue、ThreadPoolExecutor 分别为 Tomcat 自定义任务队列、线程池实现。

7.3 Tomcat自定义ThreadPoolExecutor

Tomcat 自定义线程池继承于 java.util.concurrent.ThreadPoolExecutor,并新增了一些成员变量来更高效地统计已经提交但尚未完成的任务数量(submittedCount),包括已经在队列中的任务和已经交给工作线程但还未开始执行的任务。

public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
    // 新增的submittedCount成员变量,用于统计已提交但还未完成的任务数。
    private final AtomicInteger submittedCount = new AtomicInteger(0);
    private final AtomicLong lastContextStoppedTime = new AtomicLong(0L);
    // 构造函数
    public ThreadPoolExecutor(int corePoolSize, 
    						  int maximumPoolSize, 
    						  long keepAliveTime, 
    						  TimeUnit unit, 
    						  BlockingQueue<Runnable> workQueue, 
    						  ThreadFactory threadFactory,
    						  RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
        // 预启动所有核心线程
        prestartAllCoreThreads();
    }
}

Tomcat 在自定义线程池 ThreadPoolExecutor 中重写了 execute() 方法,并实现对提交执行的任务进行 submittedCount 加一。Tomcat 在自定义 ThreadPoolExecutor 中,当线程池抛出 RejectedExecutionException 异常后,会调用 force() 方法再次向 TaskQueue 中进行添加任务的尝试。如果添加失败,则 submittedCount 减一后,再抛出 RejectedExecutionException。

7.4 Tomcat自定义任务队列

在 Tomcat 中重新定义了一个阻塞队列 TaskQueue,它继承于 LinkedBlockingQueue。在 Tomcat 中,核心线程数默认值为10,最大线程数默认为200,为了避免线程到达核心线程数后后续任务放入队列等待,Tomcat 通过自定义任务队列 TaskQueue 重写 offer() 方法实现了核心线程池数达到配置数后线程的创建。具体地,从线程池任务调度机制实现可知,当 offer() 方法返回 false 时,线程池将尝试创建新线程,从而实现任务的快速响应。TaskQueue 核心实现代码如下:

public class TaskQueue extends LinkedBlockingQueue<Runnable> {

    public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
        if ( parent==null || parent.isShutdown() ) throw new RejectedExecutionException("Executor not running, can't force a command into the queue");
        return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task is rejected
    }

    @Override
    public boolean offer(Runnable o) {
        // 1. parent为线程池,Tomcat中为自定义线程池实例
        //we can't do any checks
        if (parent==null) return super.offer(o);
        // 2. 当线程数达到最大线程数时,新提交任务入队
        //we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        // 3. 当提交的任务数小于线程池中已有的线程数时,即有空闲线程,任务入队即可
        //we have idle threads, just add it to the queue
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
        // 4. 【关键点】如果当前线程数量未达到最大线程数,直接返回false,让线程池创建新线程
        //if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        // 5. 最后的兜底,放入队列
        //if we reached here, we need to add it to the queue
        return super.offer(o);
    }   
}

7.5 Tomcat自定义任务线程

Tomcat 中通过自定义任务线程 TaskThread(继承自 Thread)实现对每个线程创建时间的记录;使用静态内部类 WrappingRunnable 对 Runnable 进行包装,用于对 StopPooledThreadException 异常类型的处理。

7.6 小总结

  1. Tomcat 为什么要自定义线程池和任务队列实现?JUC原生线程池在提交任务时,当工作线程数达到核心线程数后,继续提交任务会尝试将任务放入阻塞队列中,只有当前运行线程数未达到最大设定值且在任务队列任务满后,才会继续创建新的工作线程来处理任务,因此JUC原生线程池无法满足Tomcat快速响应的诉求。
  2. Tomcat为什么使用无界队列?Tomcat在EndPoint中通过acceptCount和maxConnections两个参数来避免过多请求积压。其中maxConnections为Tomcat在任意时刻接收和处理的最大连接数,当Tomcat接收的连接数达到maxConnections时,Acceptor不会读取accept队列中的连接;这时accept队列中的线程会一直阻塞着,直到Tomcat接收的连接数小于maxConnections(maxConnections默认为10000,如果设置为-1,则连接数不受限制)。acceptCount为accept队列的长度,当accept队列中连接的个数达到acceptCount时,即队列满,此时进来的请求一律被拒绝,默认值是100(基于Tomcat 8.5.43版本)。因此,通过acceptCount和maxConnections两个参数作用后,Tomcat默认的无界任务队列通常不会造成OOM。
/**
 * Allows the server developer to specify the acceptCount (backlog) that should be used for server sockets. By default, this value is 100.
 */
private int acceptCount = 100;
private int maxConnections = 10000;

8. 最佳实践

8.1 避免用Executors创建线程池

  1. FiexedThreadPool 和 SingleThreadPool 任务队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
  2. CachedThreadPool 和 ScheduledThreadPool 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
  3. 使用线程时,可以直接调用 ThreadPoolExecutor 的构造函数来创建线程池,并根据业务实际场景来设置 corePoolSize、blockingQueue、RejectedExecuteHandler 等参数。

8.2 避免使用局部线程池

使用局部线程池时,若任务执行完后没有执行 shutdown() 方法或有其他不当引用,极易造成系统资源耗尽。

8.3 合理设置线程池参数

8.4 增加异常处理

为了更好地发现、分析和解决问题,建议在使用多线程时增加对异常的处理,异常处理通常有下述方案:

  1. 在任务代码处增加 try…catch 异常处理。
  2. 如果使用的 Future 方式,则可通过 Future 对象的 get() 方法接收抛出的异常。
  3. 为工作线程设置 setUncaughtExceptionHandler,在 uncaughtException 方法中处理异常。

8.5 优雅关闭线程池

public void destroy() {
	try {
		poolExecutor.shutdown();
		if (!poolExecutor.awaitTermination(AWAIT_TIMEOUT, TimeUnit.SECONDS)) {
			poolExecutor.shutdownNow();
        }
	} catch (InterruptedException e) {
		// 如果当前线程被中断,重新取消所有任务。
		pool.shutdownNow();
		// 保持中断状态
		Thread.currentThread().interrupt();
    }
}

为了实现优雅停机的目标,我们应当先调用 shutdown() 方法,调用这个方法也就意味着,这个线程池不会再接收任何新的任务,但是已经提交的任务还会继续执行。之后我们还应当调用 awaitTermination() 方法,这个方法可以设定线程池在关闭之前的最大超时时间,如果在超时时间结束之前线程池能够正常关闭则会返回 true,否则,超时会返回 false。通常我们需要根据业务场景预估一个合理的超时时间,然后调用该方法。

如果 awaitTermination() 方法返回 false,但又希望尽可能在线程池关闭之后再做其他资源回收工作,可以考虑再调用一下 shutdownNow() 方法,此时队列中所有尚未被处理的任务都会被丢弃,同时会设置线程池中每个线程的中断标志位。shutdownNow() 并不保证一定可以让正在运行的线程停止工作,除非提交给线程的任务能够正确响应中断。

8.6 鹰眼上下文参数传递

// 在主线程中,开启鹰眼异步模式,并将ctx传递给多线程任务。
// 防止鹰眼链路丢失,需要传递。
RpcContext_inner ctx = EagleEye.getRpcContext();
// 开启异步模式
ctx.setAsyncMode(true);

//在线程池任务线程中,设置鹰眼rpc环境
private void runTask() {
    try {
        EagleEye.setRpcContext(ctx);
        // do something...

    } catch (Exception e) {
        log.error("requestError, params: {}", this.params, e);
    } finally {
        // 判断当前任务是否是主线程在运行,当Rejected策略为CallerRunsPolicy的时候,核对当前线程
        if (mainThread != Thread.currentThread()) {
            EagleEye.clearRpcContext();
        }
    }
}

你可能感兴趣的:(Java并发编程,多线程,java,并发编程)