从源码出发带你学线程池!

134 阅读9分钟

什么是线程?

线程是调度CPU的最小单元,也叫轻量级进程。

线程模型分类:

  1. 用户级线程 用户线程实现,不依赖操作系统核心,应用提供创建、同步、调度、和管理线程的函数来控制用户线程,不需要用户态/内核态切换,速度快、内核对用户级线程无感知,线程阻塞则进程阻塞
  2. 内核级线程 系统内核管理线程,内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞。在多处理器系统上,多线程在多处理器上并行运行,线程的创建、调度和管理由内核完成,效率比用户级线程要慢,比进程操作快。 在这里插入图片描述

什么是线程池?

线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。

为什么要使用线程池?

线程是稀缺资源,它的创建与销毁是比较严重且消耗资源的操作。而Java线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免资源过渡消耗需要设法重用线程执行多个任务。线程池就是一个线程缓存,负责对线程进行统一分配,调优与监控。

线程池优势:

  1. 重用存在的线程,减少线程创建,消亡的开销,提高性能
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控

       第一点,线程池可以解决线程生命周期的系统开销问题,同时还可以加快响应速度。因为线程池中的线程是可以复用的,我们只用少量的线程去执行大量的任务,这就大大减小了线程生命周期的开销。而且线程通常不是等接到任务后再临时创建,而是已经创建好时刻准备执行任务,这样就消除了线程创建所带来的延迟,提升了响应速度,增强了用户体验。
第二点,线程池可以统筹内存和 CPU 的使用,避免资源使用不当。线程池会根据配置和任务数量灵活地控制线程数量,不够的时候就创建,太多的时候就回收,避免线程过多导致内存溢出,或线程太少导致 CPU 资源浪费,达到了一个完美的平衡。
第三点,线程池可以统一管理资源。比如线程池可以统一管理任务队列和线程,可以统一开始或结束任务,比单个线程逐一处理任务要更方便、更易于管理,同时也有利于数据统计,比如我们可以很方便地统计出已经执行过的任务的数量。

如何使用线程池?

线程池就好比一个池塘,池塘里的水是有限且可控的,比如我们选择线程数固定数量的线程池,假设线程池有 5 个线程,但此时的任务大于 5 个,线程池会让余下的任务进行排队,而不是无限制的扩张线程数量,保障资源不会被过度消耗。如代码所示,我们往 5 个线程的线程池中放入 10000 个任务并打印当前线程名字,结果会是怎么样呢?


public class ThreadPoolDemo { 
 
    public static void main(String[] args) { 
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10000; i++) { 
            service.execute(new ThreadTask());
        } 
    	System.out.println(Thread.currentThread().getName());
    } 
 
    static class ThreadTask implements Runnable { 
 
        public void run() { 
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        } 
    } 

在这里插入图片描述

线程池工作原理:

       Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好地利用CPU资源,Java线程池的工作原理为:JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果正在运行的线程数量超过最大线程数量,则取出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现可用的线程,进而再次从队列中取出任务并执行。
线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发量,以保证系统高效安全运行。
线程在线程池中是如何复用的呢?在Java中,每个Thread类都有一个start方法。在程序调用start方法启动线程时,Java虚拟机会调用该类的run方法。在Thread类的run方法中其实调用了Runnable对象的run方法,因此可以继承Thread类,在start方法中不断循环调用传递进来的Runnable对象,程序就会不断执行run方法中的代码。可以将在循环方法之中不断获取的Runnable对象存放在Queue中,当前线程在获取下一个RUnnable对象之前可以是阻塞的,这样既能有效控制正在执行的线程个数,也能保障系统中正在等待执行的其他线程有序执行。在这里插入图片描述

线程池核心参数:

  • corePoolSize:线程池保有的最小线程数。

  • maximumPoolSize:线程池创建的最大线程数。

  • keepAliveTime:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。

  • unit:keepAliveTime 的时间单位

  • workQueue:任务队列

  • threadFactory:线程工厂对象,可以自定义如何创建线程,如给线程指定name。

  • handler:自定义任务的拒绝策略。线程池中所有线程都在忙碌,且任务队列已满,线程池就会拒绝接收再提交的任务。handler 就是拒绝策略,包括 4 种(即RejectedExecutionHandler 接口的 4个实现类)。

      a. AbortPolicy:默认的拒绝策略,直接抛出异常,throws RejectedExecutionException
       b. CallerRunsPolicy:关闭当前业务,如果被丢弃的线程任务为关闭,则执行改下次的任务。提交任务的线程自己去执行该任务
       c. DiscardPolicy:直接丢弃任务,不抛出任何异常
      d. DiscardOldestPolicy:丢弃最老的任务,加入新的任务
      e. 自定义拒绝策略。
    
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;
    }
  • 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加线程。
  • 线程池只有在任务队列填满时才创建多于 corePoolSize 的线程,如果使用的是无界队列(例如 LinkedBlockingQueue),那么由于队列不会满,所以线程数不会超过 corePoolSize。
  • 通过设置 corePoolSize 和 maximumPoolSize 为相同的值,就可以创建固定大小的线程池。
  • 通过设置 maximumPoolSize 为很高的值,例如 Integer.MAX_VALUE,就可以允许线程池创建任意多的线程。

常见的线程池:

  1. newCachedThreadPool:可缓存的线程池
// 创建线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

// 源码
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  1. newFixedThreadPool:固定大小的线程池
// 创建线程池
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);

// 源码
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  1. newScheduledThreadPool:可做任务调度的线程池
// 创建线程池
ScheduledExecutorService scheduledThreadPool = 
				Executors.newScheduledThreadPool(10);
// 源码 
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
      return new ScheduledThreadPoolExecutor(corePoolSize);
 	}
  
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
  1. newSignleThreadExecutor: 单个线程池
// 创建线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

// 源码
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  1. newWorkStealingPool: 足够大小的线程池
// 创建线程池
ExecutorService workStealingPool = Executors.newWorkStealingPool();

// 源码
public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

常用的阻塞队列:

在这里插入图片描述

为什么阿里巴巴开发规范手册中不建议使用Executors去创建线程池,而是通过ThreadPoolExecutor的方式去创建?

首先我们可以看到上面5中线程池常见的底层源码可以知道,基本底层都是return new ThreadPoolExecutor(xxxxx)来创建;其次,我们可以看到创建线程池源码中有一个队列的参数,FixedThreadPool与SingleThreadExecutor所使用的的队列是LinkedBlockingQueue,该队列的容量无上限,如果我们对任务的处理速度比较慢,那么随着请求的增多,队列中堆积的任务也会越来越多,最终大量堆积的任务会占用大量内存,并发生 OOM ,也就是OutOfMemoryError,这几乎会影响到整个程序,会造成很严重的后果。然后CachedThreadPool的线程池所使用的队列为SynchronousQueue,该队列允许创建的线程数量为Integer.MAX_VALUE;由于 CachedThreadPool 并不限制线程的数量,当任务数量特别多的时候,就可能会导致创建非常多的线程,最终超过了操作系统的上限而无法创建新线程,或者导致内存不足,并发生OOM。

如何创建合适的线程数量?

程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)

线程的平均工作时间所占比例越高,就需要越少的线程;

线程的平均等待时间所占比例越高,就需要越多的线程;

针对不同的程序,进行对应的实际测试就可以得到最合适的选择。

CPU密集型:CPU数量+1,CPU数量的获取方式:Runtime.availableProcessors方法获取。

IO密集型:CPU数量CPU利用率(1+线程等待时间/线程CPU时间)

如何正确的关闭线程池?

/**
  * 它可以安全地关闭一个线程池,调用 shutdown() 方法之后线程池并不是立刻就被关闭,
  * 因为这时线程池中可能还有很多任务正在被执行,或是任务队列中有大量正在等待被执行的任务,
  * 调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。
  * 但这并不代表 shutdown() 操作是没有任何效果的,调用 shutdown() 方法后如果还有新的任务被提交,
  * 线程池则会根据拒绝策略直接拒绝后续新提交的任务。
  */
void shutdown;

/**
  * 可以返回 true 或者 false 来判断线程池是否已经开始了关闭工作,也就是是否执行了 shutdown 
  * 或者 shutdownNow 方法。这里需要注意,如果调用 isShutdown() 方法的返回的结果为 true 
  * 并不代表线程池此时已经彻底关闭了,这仅仅代表线程池开始了关闭的流程,也就是说,
  * 此时可能线程池中依然有线程在执行任务,队列里也可能有等待被执行的任务
  */
boolean isShutdown;

/**
  * 这个方法可以检测线程池是否真正“终结”了,这不仅代表线程池已关闭,
  * 同时代表线程池中的所有任务都已经都执行完毕了,调用 isTerminated() 方法才会返回 true,
  * 这表示线程池已关闭并且线程池内部是空的,所有剩余的任务都执行完毕了。
  */
boolean isTerminated;

/**
 * 调用 awaitTermination 方法后当前线程会尝试等待一段指定的时间,如果在等待时间内,
 * 线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正“终结”了,那么方法就返回 true,
 * 否则超时返回 fasle。
 */
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

/**
 * 在执行 shutdownNow 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,
 * 尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回
 */
List<Runnable> shutdownNow;

如何提交任务?

  1. 提交无返回值的任务(execute)
public void execute(Runnable command) {  }
  1. 提交有返回值的任务(submit())
public Future<?> submit(Runnable task) {
        return schedule(task, 0, NANOSECONDS);
    }

execute 源码分析:

public void execute(Runnable command) {
	  //如果传入的Runnable的空,就抛出异常
      if (command == null)
          throw new NullPointerException();
      int c = ctl.get();
      // 当前线程数是否小于核心线程数,如果小于核心线程数就调用 addWorker() 
      // 方法增加一个 Worker,这里的 Worker 就可以理解为一个线程
      if (workerCountOf(c) < corePoolSize) {
          if (addWorker(command, true))
              return;
          c = ctl.get();
      }
      // 当前线程数大于或等于核心线程数或者 addWorker 失败了,那么就需要通过 
      // if (isRunning(c) && workQueue.offer(command)) 检查线程池状态是否为 Running,
      // 如果线程池状态是 Running 就把任务放入任务队列中,也就是 workQueue.offer(command)。
      // 如果线程池已经不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,
      // 并执行拒绝策略,
      if (isRunning(c) && workQueue.offer(command)) {
          int recheck = ctl.get();
          if (! isRunning(recheck) && remove(command))
              reject(command);
          else if (workerCountOf(recheck) == 0)
          //能进入这个 else 说明前面判断到线程池状态为 Running,那么当任务被添加进来之后就需要防止
          //没有可执行线程的情况发生(比如之前的线程被回收了或意外终止了),所以此时如果检查当前线程
          //数为 0,也就是 workerCountOf**(recheck) == 0,那就执行 addWorker() 方法新建线程。
              addWorker(null, false);
      }
      // 说明线程池不是 Running 状态或线程数大于或等于核心线程数并且任务队列已经满了,根据规则,
      // 此时需要添加新线程,直到线程数达到“最大线程数”,所以此时就会再次调用 addWorker 方法并将
      // 第二个参数传入 false,传入 false 代表增加线程时判断当前线程数是否少于 maxPoolSize,
      // 小于则增加新线程,大于等于则不增加,也就是以 maxPoolSize 为上限创建新的 worker;
      // addWorker 方法如果返回 true 代表添加成功,如果返回 false 代表任务添加失败,
      // 说明当前线程数已经达到 maxPoolSize,然后执行拒绝策略 reject 方法。
      // 如果执行到这里线程池的状态不是 Running,那么 addWorker 会失败并返回 false,
      // 所以也会执行拒绝策略 reject 方法
      else if (!addWorker(command, false))
          reject(command);
  }

// 添加work
private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

    // 执行拒绝策略
	final void reject(Runnable command) {
        handler.rejectedExecution(command, this);
    }

实现线程复用的逻辑主要在一个不停循环的 while 循环体中。通过取 Worker 的 firstTask 或者通过getTask 方法从 workQueue 中获取待执行的任务。直接调用 task 的 run 方法来执行具体的任务(而不是新建线程)。 在这里,我们找到了最终的实现,通过取 Worker 的 firstTask 或者 getTask方法从 workQueue 中取出了新任务,并直接调用 Runnable 的 run 方法来执行任务,也就是如之前所说的,每个线程都始终在一个大循环中,反复获取任务,然后执行任务,从而实现了线程的复用。