Java并发机制(八)—— 线程池

1 线程池简介

Java中大多数多线程程序都是通过实现Runnable来完成的,对于Android来说也不例外,当涉及到需要开启线程去完成某件事时,可以这样写:

new Thread(new Runnable() {  @Override  public void run() {  }});

这段代码创建了一个线程并执行,它在任务结束后GC会自动回收该线程。这样的实现方式,在线程不多的程序中不会有太大的问题,假如这个程序有很多地方需要开启大量的线程来处理任务,如果还用上述的方法去创建线程处理的话,那么将导致系统的性能表现的很糟糕,更别说在内存有限的移动设备上了,主要的影响如下:

  • 线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,这些时间的消耗则比较明显,将导致性能上的缺失
  • 大量的线程创建、执行和销毁是非常耗CPU和内存的,这样将直接影响系统的吞吐量,导致性能急剧下降,如果内存资源占用比较多,还可能会导致OOM
  • 大量的线程创建和销毁很容易导致GC频繁的执行,从而发生内存抖动现象,而发生了内存抖动,对于移动端来说,最大的影响就是造成界面卡顿

对于上面所描述的问题,解决方法是,重用已有的线程,减少线程的创建。

通过new Thread().start()方式创建线程去处理任务的弊端,而为了解决这些问题,Java为我们提供了ExecutorService线程池来优化和管理线程的使用,线程池的基本作用就是进行线程的复用。

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来以下好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
  • Java内置了一套ExecutorService线程池相关的api,可以更方便的控制线程的最大的并发数、线程的定时任务、线程的顺序执行等。

注意:线程池不能简单的理解成一个线程的集合。线程池除了是线程的容器之外,还负责管理线程的生命周期、资源开销 等问题。

有一种fork-join式线程池(任务抢占式线程池),适用于可分割的任务,实现方式和普通线程池相似,但是其任务可以“抢占”:比如某个线程提前完成了自己的任务,而其它线程可能还未完成,使用普通线程池的话,这个线程不会帮助其它线程完成任务,而fork-join式线程池,这个线程的任务执行完毕后可以帮其它线程做任务。

2 源码分析

ExecutorService是一个接口,其实如果从真正意义上来说,它可能叫做线程池的服务,因为它提供了众多接口api来控制线程池中的线程。

public interface ExecutorService extends Executor {
  void shutdown();
  List<Runnable> shutdownNow();
  boolean isShutdown();
  boolean isTerminated();
  boolean awaitTermination(long timeout, TimeUnit unit)
    throws InterruptedException;
  <T> Future<T> submit(Callable<T> task);
  <T> Future<T> submit(Runnable task, T result);
  Future<?> submit(Runnable task);
  <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
    throws InterruptedException;
  <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                long timeout, TimeUnit unit)
    throws InterruptedException;
  <T> T invokeAny(Collection<? extends Callable<T>> tasks)
    throws InterruptedException, ExecutionException;
  <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                  long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException;
}

public interface Executor {
    void execute(Runnable command);
}

以下是ExecutorService的继承关系:

线程池的继承关系

ThreadPoolExecutorScheduledThreadPoolExecutorExecutorService的具体实现类:

Executor 的继承关系

public interface ExecutorService extends Executor { }

public abstract class AbstractExecutorService implements ExecutorService { }

public class ThreadPoolExecutor extends AbstractExecutorService { }

public interface ScheduledExecutorService extends ExecutorService { }

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor
        implements ScheduledExecutorService { }
2.1ThreadPoolExecutor

ThreadPoolExecutorExecutorService的一个实现类,也是Java种最常用的线程类,ThreadPoolExecutor内部维持了一个线程池,可以执行给定的任务。

2.1.1 构造方法

要创建一个线程池只需要new ThreadPoolExecutor(...);,就可以创建了,但是这样创建线程池的话,需要配置一堆配置,非常麻烦:

public class ThreadPoolExecutor extends AbstractExecutorService {
  public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler) {}
}

参数的作用

  • corePoolSize:线程池中的核心(最小)线程数量
  • maximumPoolSize:线程池中的最大线程数量
  • keepAliveTime:“保持活动时间”,它起作用必须在一个前提下,就是当线程池中的 线程数量超过了corePoolSize时,它表示多余的空闲线程的存活时间,即:多余的空闲线程在keepAliveTime时间内没有任务的话则被销毁。这个主要应用在缓存线程池中
  • unit:它是一个枚举类型,表示keepAliveTime的单位,常用的如:TimeUnit.SECONDS(秒)TimeUnit.MILLISECONDS(毫秒)
  • workQueue:任务队列,主要用来创建已经提交但未被执行的任务,不同的线程池采用的排队策略不一样,默认是LinkedBlockingQueue<Runnable>
  • threadFactory:线程工厂,用来创建线程池中的线程,通常用默认的即
    handler:通常叫做拒绝策略
    • 在线程池已经关闭的情况下
    • 任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务
      在上面的两种情况下,只要满足其中一种时,在使用execute()来提交新的任务时将会拒绝,而默认的拒绝策略时抛出一个RejectedExecutionExecption异常

线程数量控制

当一个任务被添加到线程池,具体线程分配方式是:

  • 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务
  • 如果此时线程池中的数量等于corePoolSize,但缓冲队列workQueue未满,那么任务被放入缓冲队列
  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue已满,并且线程池中的数量小于maximumPoolSize,那么创建新的线程来处理被添加的任务
  • 如果此时线程池的数量大于corePoolSize,缓冲队列workQueue已满,并且线程池的数量等于maximumPoolSize,那么通过handler所指定的策略来处理任务。 也就是说,任务处理的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务
  • 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将终止

任务执行

WorkQueue

它是一个BlockingQueue<Runnable>对象,而范型则限定它是用来存放Runnable对象的,不同的线程池它的任务队列实现肯定是不一样的,所以,保证不同线程池有着不同的功能的核心就是这个workQueue的实现。在创建线程池的工厂方法中,针对不同的线程池传入的workQueue也不一样,下面总结一下这五种线程池分别用的是什么BlockingQueue

  • newFixedThreadPool()->LinkedBlockingQueue
  • newSingleThreadExecutor()->LinkedBlockingQueue
  • newCachedThreadPool()->SynchronousQueue
  • newScheduledThreadPool()->DelayedWorkQueue
  • newSingleThreadScheduledExecutor()->DelayedWorkQueue

这些队列分别表示:

  • LinkedBlockingQueue:无界的队列
  • SynchronousQueue:直接提交的队列
  • DelayedWorkQueue:等待队列

当然实现了BlockingQueue接口的队列还有:ArrayBlockingQueue(有界的队列)、PriorityBlockingQueue(优先级队列)

handler

handler是线程池拒绝处理任务的方式,主要有四种类型:

  • ThreadPoolExecutor.AbortPolicy():系统默认,抛出java.util.concurrent.RejectedExecutionException异常
  • ThreadPoolExecutor.CallerRunsPolicy():当抛出RejectedExecutionException异常时,会调用rejectedExecution方法
  • ThreadPoolExecutor.DiscardOldestPolicy():抛弃旧时任务
  • ThreadPoolExecutor.DiscardPolicy():抛弃当前的任务
2.1.2 ThreadPoolExecutor.execute()

在查看 ThreadPoolExecutor.execute() 方法之前,首先需要了解几个变量:

// /libcore/ojluni/src/main/java/java/util/concurrent/ThreadPoolExecutor.java
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static int ctlOf(int rs, int wc) { return rs | wc; }

// 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;

ctl 表示两个概念:runState — 当前线程池的五种状态:RUNNABLESHUTDOWNSTOPTIDYINGTERMINATEDworkerCount — 当前有效的线程数;

以下是 TheadPoolExecutor.execute() 方法的源代码:

// /libcore/ojluni/src/main/java/java/util/concurrent/ThreadPoolExecutor.java
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
   
  	// 获取当前有效的线程数和线程池的状态
    int c = ctl.get();
    // 1 如果当前正在运行的线程数小于核心线程数,则创建一个新的线程去执行任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true)) // 创建工作线程执行任务
            return;
        c = ctl.get();
    }
  
  	// 运行到此处,说明正在运行的线程数等于核心线程数,此时的任务应该添加到任务队列中
  	// 2 判断核心线程都在运行,并且将线程成功插入到任务队列中
    if (isRunning(c) && workQueue.offer(command)) { 
        int recheck = ctl.get();
        if (!isRunning(recheck) && remove(command)) 
            reject(command); // 抛出异常
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command); // 抛出异常
}

private static int workerCountOf(int c)  { return c & CAPACITY; }

private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}

public boolean remove(Runnable task) {
    boolean removed = workQueue.remove(task);
    tryTerminate(); // In case SHUTDOWN and now empty
    return removed;
}

在注释 1 处判断正在运行的线程是否达到核心线程数,如果没有则通过 ThreadPoolExecutor.addWorker(Runnable) 方法创建工作线程执行任务。在注释 2 处判断核心线程都在运行,则添加到工作队列中。

以下是 ThreadPoolExecutor.addWorker(Runnable, boolean)的相关源码:

// /libcore/ojluni/src/main/java/java/util/concurrent/ThreadPoolExecutor.java
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
      	// 首先会再次检查线程池是否处于运行状态,正在运行的线程是否没有达到核心线程数,都满足条件后则会调用 compareAndIncrementWorkerCount 先将正在运行的线程数 +1,数量自增成功则跳出循环,自增失败则继续从头开始继续循环
        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;
          	// 1 将正在运行的线程数 +1,数量自增成功则跳出循环,自增失败则继续从头继续循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
      	// 2 将线程封装成 Worker 工作线程
        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()) 
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size(); // 工作线程数量
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start(); // 3 在被构造为 Worker 工作线程,且被加入到工作线程集合后,执行线程任务,注意:这里的 start() 方法实际上执行的是 Worker 中的 run 方法
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted) // 未能成功创建执行工作线程
            addWorkerFailed(w); // 在启动工作线程失败后,经工作线程从集合中移除
    }
    return workerStarted;
}

工作线程被成功添加到工作线程集合中后,开始执行 Thread.start() 方法,这里实际上调用的是 Worker.run() 方法,以下是相关代码:

// /libcore/ojluni/src/main/java/java/util/concurrent/ThreadPoolExecutor.java
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    
    private static final long serialVersionUID = 6138294804551838833L;

    final Thread thread;
    Runnable firstTask;
    volatile long completedTasks;

    Worker(Runnable firstTask) {
        setState(-1); // 1 设置 AQS 的同步状态为 -1,禁止中断,直到调用 runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this); // 2 通过线程工厂来创建一个线程,将自身的 Runnable 传递过去
    }

    public void run() {
        runWorker(this); // 3 运行工作线程
    }
    ...
}

Worker 继承了 AbstractQueuedSynchronizer (AQS),同时实现了 Runnable,所以它同时具备这两者的特性。在注释 3 处调用 ThreadPoolExecutor.runWorker(this) 运行工作线程。

// /libcore/ojluni/src/main/java/java/util/concurrent/ThreadPoolExecutor.java
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) { // 1
            w.lock();
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

在此方法中,Worker 在执行完任务后,还会循环获取任务列表中的任务执行,也就是说,Worker 不仅仅是在执行完给它的任务就释放或者结束,它不会闲着,而是继续从任务队列中获取任务中获取任务,直到没有任务执行,它才会退出循环。

以下是 ThreadPoolExecutor.execute(Runnable) 的调用流程:

execute 方法调用流程

2.2 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutorExecutorService的另一个实现类,直接继承自ScheduledExecutorService

2.2.1 ExecutorService

ScheduledExecutorService接口继承了ExecutorService,它的最主要的功能就是可以对其中的任务进行调度,比如延迟执行、定时执行等等:

public interface ScheduledExecutorService extends ExecutorService {
  public ScheduledFuture<?> schedule(Runnable task, long delay, TimeUnit unit);

  public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
  
  public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,
                           long period, TimeUnit unit);
  
  public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,
                           long delay, TimeUnit unit);
}

schedule(Runnable task, long delay, TimeUnit unit)

在指定延迟之后运行task,但是这个方法无法获知command的执行结果。

schedule(Callable<V> task, long delay, TimeUnit unit)

这个方法与schedule(Runnable task, long delay, TimeUnit unit)类似,也是在指定延迟之后运行task,不过它接收的事一个Callable实例,此方法会返回一个ScheduledFuture对象,通过ScheduleFuture可以取消一个未执行的task,也可以获取这个task的执行结果:

public static void main(String[] args) throws ExecutionException, InterruptedException {
  ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
  ScheduledFuture scheduledFuture = scheduledExecutorService.schedule(new Callable<Object>() {
    @Override
    public Object call() throws Exception {
      System.out.println("Executed!");
      return "Called";
    }
  }, 5, TimeUnit.SECONDS);
  System.out.println("result = " + scheduledFuture.get());
  scheduledExecutorService.shutdown();
}
// Executed!
// result = Called

scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)

这个方法的作用是周期性的调度task执行。task第一次执行的延迟根据initialDelay参数确定,以后每一次执行都间隔period时长。如果task的执行时间大于定义的period,那么下一个线程将在当前线程完成之后再执行,整个调度保证不会出现一个以上任务同时执行。

scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)

scheduleWithFixedDelay的参数和scheduleAtFixedRate的参数完全一致,它们的不同之处在于对period调度周期的解释。

scheduleAtFixedRatepeirod指的是两个任务开始执行的时间间隔,也就是当前任务的开始执行时间和下个任务的开始执行是假之间的间隔;而scheduleWithFixedDelay中,period指的是当前任务的结束执行时间到下个任务的开始执行时间。

2.2.2 ThreadPoolExecutor

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,以下是构造函数:

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor 
  		implements ScheduledExecutorService {
  
  public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
  }
  public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
  }
  public ScheduledThreadPoolExecutor(int corePoolSize, RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), handler);
  }
  public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory,
                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory, handler);
  }
}

参数类比ThreadPoolExecutor

2.3 Executors

官方并不推荐直接使用ThreadPoolExecutor这种方法来创建线程池,而是推荐使用Executors的工厂方法来创建线程池,Executors类是官方提供的一个工厂类,它里面封装了众多功能不一样的线程池,从而使得我们创建线程池非常的方便,主要提供了如下五种功能不一样的线程池:

  • newSingleThreadExecutor:返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务;
  • newSingleThreadScheduledExecutor:返回一个可以控制线程池内线程定时或周期性执行某任务的线程池,并且该线程池大小为1
  • newFixedThreadPool:返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始至终都是那几个固定的线程在工作,该线程可池以控制线程的最大并发数;
  • newScheduledThreadPool:返回一个可以控制线程池内线程定时或周期性执行某任务的线程池,可以指定线程池的大小。
  • newCachedThreadPool:返回一个可以根据实际情况调整线程池中线程的数量的线程池,即该线程池中的线程数量不确定,是根据实际情况动态调整的;
2.3.1 newSingleThreadExecutor

返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。

public class Executors {

  public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
      (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue<Runnable>()));
  }

  private static class FinalizableDelegatedExecutorService extends DelegatedExecutorService {
    FinalizableDelegatedExecutorService(ExecutorService executor) {
      super(executor);
    }
    protected void finalize() {
      super.shutdown();
    }
  }
  
  private static class DelegatedExecutorService extends AbstractExecutorService { }
  
}

public abstract class AbstractExecutorService implements ExecutorService { }

从上面的代码中可以看出,单线程池中使用的是无界队列。由于单线程工作只能处理一个任务,所以后面的所有任务都被阻塞到工作队列中,只能一个个的执行。

2.3.2 newFixedThreadPool

返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始至终都是那几个固定的线程在工作,该线程可池以控制线程的最大并发数。

🌰:假如有一个新任务提交时,如果该线程池中有空闲的线程则立即使用该线程来处理任务,如果没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按照FIFO方式处理任务列表中的任务。

public class Executors {

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

Demo

class Test {
  public static void main(String[] args) {
    ExecutorService pool = Executors.newFixedThreadPool(2);
    Thread t1 = new TestThread();
    Thread t2 = new TestThread();
    Thread t3 = new TestThread();
    Thread t4 = new TestThread();
    Thread t5 = new TestThread();
    pool.execute(t1);
    pool.execute(t2);
    pool.execute(t3);
    pool.execute(t4);
    pool.execute(t5);
  }
}

class TestThread extends Thread {
  @Override
  public void run() {
    System.out.println(Thread.currentThread().getName() + "正在执行...");
  }
}
2.3.3 newCachedThreadPool

返回一个可以根据实际情况调整线程池中线程的数量的线程池,即该线程池中的线程数量不确定,是根据实际情况动态调整的。

🌰:假如该线程池中的所有线程都正在工作,而此时又有新任务提交,那么将会创建新的线程去处理该任务,而此时假如之前有一些线程完成了任务,现在又有新任务提交,那么将不会创建新线程去处理,而是复用空闲的线程去处理新任务,那么此时又有疑问了,那这样来说该线程池的线程岂不是会越集越多?其实并不会,因为线程池中的线程都有一个“保持活动时间”的参数,通过配置它,如果线程池中的空闲线程的空闲时间超过该“保存活动时间”则立即停止该线程,而该线程池默认的“保持活动时间”为60s

public class Executors {
  public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
  }
}

在壮烈使用的是 SynchronousQueue ,直接提交的队列,即没有工作队列,任务进来就执行,线程数量不够就创建。与前面两个线程池不同的地方就是,空闲的线程会被回收掉,空闲的时间是 60s,这个适合执行很多短期的异步小程序或者负载较轻的服务器。

2.3.4 newSingleThreadScheduledExecutor

返回一个可以控制线程池内线程定时或周期性执行某任务的线程池,并且该线程池大小为1

public class Executors {
  public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
      (new ScheduledThreadPoolExecutor(1));
  }
  
  private static class DelegatedScheduledExecutorService extends DelegatedExecutorService
         implements ScheduledExecutorService { }
  
  private static class DelegatedExecutorService extends AbstractExecutorService { }
}

public abstract class AbstractExecutorService implements ExecutorService { }

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor
  implements ScheduledExecutorService {
  public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
  }
}

public interface ScheduledExecutorService extends ExecutorService {  }

public interface ExecutorService extends Executor { }
2.3.5 newScheduledThreadPool

返回一个可以控制线程池内线程定时或周期性执行某任务的线程池,可以指定线程池的大小。

public class Executors {
  public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
  }
}

其实它们的内部还是通过new ThreadPoolExecutor的方式来创建线程池的。

接下来就是获取这五种线程池,通过Executors的工厂方法来获取:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();

3 线程池的实现原理

当向线程池提交一个任务之后,线程池是如何处理这个任务的呢?以下是线程池的主要处理流程:

线程池的主要处理流程

  1. 线程池判断核心线程池里的线程是否都在执行任务。 如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
  2. 线程池判断工作队列是否已经满。 如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
  3. 线程池判断线程池的线程是否都处于工作状态。 如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

以下是ThreadPoolExecutor执行execute()方法的示意图:

ThreadPoolExecutor执行示意图

ThreadPoolExecutor执行execute方法分下面4种情况:

  • 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意:执行这一步骤需要获取全局锁)。
  • 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue
  • 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
  • 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后 (当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而 步骤2不需要获取全局锁。

源码分析:上面的流程分析让我们很直观地了解了线程池的工作原理,再通过源代码来看看是如何实现的,线程池执行任务的方法如下:

public class ThreadPoolExecutor extends AbstractExecutorService {
  public void execute(Runnable command) {
    if (command == null)
      throw new NullPointerException(); 
    // 如果线程数小于基本线程数,则创建线程并执行当前任务
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) { 
      // 如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队列中。
      if (runState == RUNNING && workQueue.offer(command)) {
        if (runState != RUNNING || poolSize == 0) 
          ensureQueuedTaskHandled(command);
      }
      // 如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量, // 则创建一个线程执行任务。
      else if (!addIfUnderMaximumPoolSize(command))
        // 抛出RejectedExecutionException异常
        reject(command); // is shutdown or saturated
    } 
  }
}

工作线程:线程池创建线程时,会将线程封装成工作线程WorkerWorker在执行完任务后,还会循环获取工作队列里的任务来执行。我们可以从Worker类的run()方法里看到这点。

public void run() {
  try {
    Runnable task = firstTask;
    firstTask = null;
    while (task != null || (task = getTask()) != null) {
      runTask(task);
      task = null;
    } 
  } finally {
    workerDone(this);
  }
}

ThreadPoolExecutor中线程执行任务的示意图:

ThreadPoolExecutor执行任务示意图

线程池中的线程执行任务分两种情况:

  • execute()方法中创建一个线程时,会让这个线程执行当前任务。
  • 这个线程执行完上图中1的任务后,会反复从BlockingQueue获取任务来执行。

4 线程池的使用

4.1 线程池的创建

可以通过ThreadPoolExecutor来创建一个线程池:

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,  milliseconds, runnableTaskQueue, handler);

创建一个线程池时需要输入几个参数,如下:

  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任 务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法, 线程池会提前创建并启动所有基本线程。
  • runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue 静态工厂方法Executors.newFixedThreadPool()使用了这个队列
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue 静态工 厂方法Executors.newCachedThreadPool使用了这个队列
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列
  • maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。 值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。 使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字:new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。JDK 1.5Java线程池框架提供了以下4种策略。
    • AbortPolicy:直接抛出异常
    • CallerRunsPolicy:只用调用者所在线程来运行任务
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
    • DiscardPolicy:不处理,丢弃掉

也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。

  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。 所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
  • TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。
4.2 向线程池提交任务

可以使用两个方法向线程池提交任务,分别为execute()submit()方法。

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。 通过以下代码可知execute()方法输入的任务是一个Runnable类的实例:

threadsPool.execute(new Runnable() { 
  @Override
  public void run() {
    // TODO Auto-generated method stub
  } 
});

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

Future<Object> future = executor.submit(harReturnValuetask); 
try {
  Object s = future.get(); 
} catch (InterruptedException e) {
  // 处理中断异常
} catch (ExecutionException e) {
  // 处理无法执行任务异常 
} finally {
  // 关闭线程池 
  executor.shutdown();
}
4.3 关闭线程池

可以通过调用线程池的shutdownshutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别:

  • shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
  • shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

public class ThreadPoolExecutor extends AbstractExecutorService {
  
  public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
      checkShutdownAccess();
      advanceRunState(SHUTDOWN);
      interruptIdleWorkers();
      onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
      mainLock.unlock();
    }
    tryTerminate();
  }
  
  public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
      checkShutdownAccess();
      advanceRunState(STOP);
      interruptWorkers();
      tasks = drainQueue();
    } finally {
      mainLock.unlock();
    }
    tryTerminate();
    return tasks;
  }
}
4.4 合理地配置线程池

要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析:

  • 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
  • 任务的优先级:高、中和低。
  • 任务的执行时间:长、中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu + 1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应置尽可能多的线程,如2 * Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量 将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。

注意:如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU

建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次,我们系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里。如果当时我们设置成无界队列,那么线程池的队列就会越来越多, 有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然,我们的系统所有的任务是用单独的服务器部署的,我们使用不同规模的线程池完成不同类型的任务,但是出现这样问题时也会影响到其他任务。

4.5 线程池的监控

如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题。可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性:

  • taskCount:线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount
  • largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是 否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销 毁,所以这个大小只增不减。
  • getActiveCount:获取活动的线程数。

通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecuteafterExecuteterminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。 这几个方法在线程池里是空方法。

protected void beforeExecute(Thread t, Runnable r) { }
4.6 RejectedExecutionHandler 饱和策略

当线程池和队列都满了,说明线程池处于饱和状态,对于新提交的任务需要采用一种特殊的策略来处理,这个策略的默认配置是 AbortPolicy,表示无法处理新的任务而抛出异常。Java 提供了 4 种策略:

  • AbortPolicy:默认策略,当线程池和队列满的时候直接丢掉这个任务并抛出 RejectExecutionHandler 异常;
  • DiscardPolicy:当线程池和队列满时,丢掉这个任务,不抛出异常;
  • DiscardOldestPolicy:当线程池和队列满时,丢弃队列中最老的一个任务,再尝试加入队列中;
  • CallerRunsPolicy:当线程池和队列满的时,会调用当前线程池所在的线程去执行任务;

通过以下代码来测试相应的饱和策略:

public static void main(String[] args) {
    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(3);
    RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
  	// RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
  	// RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
  	// RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 5, TimeUnit.SECONDS, queue, handler);
    for (int i = 0; i < 20; i++) {
        threadPoolExecutor.execute(new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(0);
                    System.out.println(Thread.currentThread().getName() + " is running");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "Thread".concat(i + "")));
    }
    threadPoolExecutor.shutdown();
}

创建线程池的具体分配为:核心线程数为 3 个,最大线程数为 5 个,工作队列的长度为 2。从打印的日志可以看出,在未达到核心线程数的时候都在创建新的线程执行任务,在达到核心线程数之后,就将任务加入到工作队列中了,工作队列中的任务数达到 2 个后,会继续创建新的普通线程执行任务,直到达到最大线程数。再到后面的任务会根据配置的饱和策略来处理。

使用默认的构造方法 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 5, TimeUnit.SECONDS, queue); 或者 RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();,直接抛出异常:

AbortPolicy

使用 RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();

DiscardPolicy

使用 RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();

DiscardOldestPolicy

使用 RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();

CallerRunsPolicy

为什么不推荐使用 Executors 直接创建线程池?

对于 Executors.newSingleThreadPool()/Executors.newFixedThreadPool() 方法,使用的缓存队列 LinkedBlockingQueue,这种队列没有设置固定的容量,是无界的,这样就会导致其无限的增大,最终内存会被消耗殆尽。

对于 Executors.newCachedThreadPool()/Executors.newScheduledThreadPool(),其 maximumPoolSize 的大小为 Integer.MAX_VALUE,可以任务是无限大。使用 Executors.newCachedThreadPool() 方法不会缓存到队列中。随着执行线程的数量不断增多,如果线程没有及时结束,最终内存也会被消耗殆尽。

参考

12.ThreadPoolExecutor线程池原理及其execute方法
线程池的4种拒绝策略
线程池-四种拒绝策略总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值