Java多线程总结(6)— 线程池的基本使用和执行流程分析

1 线程池的实现原理及基本类结构

  合理利用线程池能够带来三个好处。

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

  Executor线程池框架的最大优点是把任务的提交和执行解耦。客户端将要执行的任务封装成Task,然后提交即可。而Task如何执行客户端则是透明的。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果。
  下图是线程池所涉及到的所有类的结构图(右键查看大图),先从整体把握下:
Java多线程总结(6)— 线程池的基本使用和执行流程分析_第1张图片
              图1 线程池实现原理类结构图

  上面这个图是很复杂的,涉及到了线程池内部实现原理的所有类,不利于我们理解线程池如何使用。我们先从客户端的角度出发,看看客户端使用线程池所涉及到的类结构图:

              图2 线程池使用的基本类结构图

  从图一可知,实际的线程池类是实现ExecutorService接口的类,有ThreadPoolExecutor、ForkJoinPool和ScheduledThreadPoolExecutor。下面以常用的ThreadPoolExecutor为例讲解。

2 线程池实现步骤

2.1 线程池的创建

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

  参数说明:

  • corePoolSize(线程池的基本线程数): the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set.当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  • maximumPoolSize(线程池最大线程数): the maximum number of threads to allow in the pool.线程池允许创建的最大线程数。如果任务队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
  • keepAliveTime(线程活动保持时间):when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  • TimeUnit(线程活动保持时间的单位):the time unit for the keepAliveTime argument. 可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
  • workQueue(任务队列):the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
      ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
      LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
      SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
      PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
      AbortPolicy:直接抛出异常。
      CallerRunsPolicy:只用调用者所在线程来运行任务。
      DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
      DiscardPolicy:不处理,丢弃掉。
      当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

  由此可见,创建一个线程所需的参数很多,线程池为我们提供了类Executors的静态工厂方法以创建不同类型的线程池。
- newFixedThreadPool可以生成固定大小的线程池;
- newCachedThreadPool可以生成一个无界、可以自动回收的线程池;
- newSingleThreadScheduledExecutor可以生成一个单个线程的线程池;
- newScheduledThreadPool还可以生成支持周期任务的线程池。

2.2 向线程池提交任务

  有两种方式提交任务:
1.使用void execute(Runnable command)方法提交任务
execute方法返回类型为void,所以没有办法判断任务是否被线程池执行成功。

Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("Task is running by " + Thread.currentThread().getName());
        System.out.println("线程池正在执行的线程数:" + threadPoolExecutor.getActiveCount());
    }
};
threadPool.execute(task);

2.使用submit方法提交任务
Future submit(Runnable task); Future submit(Runnable task, T result);Future submit(Callable task);会返回一个Future,可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

Future future = threadPool.submit(task);
try {
    Object result = future.get();
    System.out.println("任务是否完成:" + future.isDone());
    System.out.println("返回的结果为:" + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
} finally {
    // 关闭线程池
    threadPool.shutdown();
}

2.3 线程池关闭

  1. shutdown()方法:这个方法会平滑地关闭ExecutorService,当我们调用这个方法时,ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。
  2. awaitTermination(long timeout, TimeUnit unit)方法:这个方法有两个参数,一个是timeout即超时时间,另一个是unit即时间单位。这个方法会使当前关闭线程池的线程等待timeout时长,当超过timeout时间后,则去监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。
  3. shutdownNow()方法:这个方法会强制关闭ExecutorService,它将取消所有运行中的任务和在工作队列中等待的任务,这个方法返回一个List列表,列表中返回的是等待在工作队列中的任务。
// 4. 关闭线程池
threadPool.shutdown();
// hreadPool.shutdownNow();
System.out.println("线程池是否关闭:" + threadPool.isShutdown());
try {
    //当前线程阻塞10ms后,去检测线程池是否终止,终止则返回true
    while(!threadPool.awaitTermination(10, TimeUnit.MILLISECONDS)) {
        System.out.println("检测线程池是否终止:" + threadPool.isTerminated());
    }
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("线程池是否终止:" + threadPool.isTerminated());

  完整案例:

package com.markliu.concurrent.threadpool;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo1 {

    public static void main(String[] args) {
        /*
         * 1. 创建线程池
         * 
         * 创建一个固定线程数目的线程池。corePoolSize = maximumPoolSize = 5
         * 即线程池的基本线程数和最大线程数相等。
         * 相当于:
         * new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue());
         */
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        /*
         *  2. 封装任务并提交给线程池
         */
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) threadPool;
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println("Task is running by " + Thread.currentThread().getName());
                System.out.println("线程池正在执行的线程数:" + threadPoolExecutor.getActiveCount());
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        /*
         * Starts all core threads, causing them to idly wait for work. 
         * This overrides the default policy of starting core threads 
         * only when new tasks are executed.
         */
        int count = threadPoolExecutor.prestartAllCoreThreads();
        System.out.println("开启的所有core线程数:" + count);
        System.out.println("线程池当前线程数:" + threadPoolExecutor.getPoolSize());
        System.out.println("线程池的core number of threads:" + threadPoolExecutor.getCorePoolSize());
        System.out.println("线程池中的最大线程数:" + threadPoolExecutor.getLargestPoolSize());
        // 3. 执行,获取返回结果
        /**
         * execute方式提交任务
         */
        // threadPool.execute(task);
        /**
         * submit方式提交任务
         */
        Future future = threadPool.submit(task);
        try {
            // 阻塞,等待线程执行完成,并获得结果
            Object result = future.get();
            System.out.println("任务是否完成:" + future.isDone());
            System.out.println("返回的结果为:" + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            System.out.println("线程池中已经执行完的任务数:" + threadPoolExecutor.getCompletedTaskCount());
            // 4. 关闭线程池
            /*
             * shutdown方法平滑地关闭线程池,将线程池的状态设为:SHUTDOWN
             * 停止接受任何新的任务且等待已经提交的任务执行完成,当所有已经
             * 提交的任务执行完毕后将会关闭线程池
             */
            threadPool.shutdown();
            /*
             *  shutdownNow方法强制关闭线程池,将线程池状态设置为:STOP
             *  取消所有运行中的任务和在工作队列中等待的任务,并返回所有未执行的任务List
             */
            // hreadPool.shutdownNow();
            System.out.println("线程池是否关闭:" + threadPool.isShutdown());
            try {
                //当前线程阻塞10ms后,去检测线程池是否终止,终止则返回true
                while(!threadPool.awaitTermination(10, TimeUnit.MILLISECONDS)) {
                    System.out.println("检测线程池是否终止:" + threadPool.isTerminated());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程池是否终止:" + threadPool.isTerminated());
        }
    }
}

3 线程池的执行流程分析

  线程池的主要工作流程如下图:
    Java多线程总结(6)— 线程池的基本使用和执行流程分析_第2张图片
  当提交一个新任务到线程池时,线程池的处理流程如下:
- 首先线程池判断“基本线程池”(corePoolSize)是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
- 其次线程池判断工作队列(workQueue)是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
- 最后线程池判断整个线程池的线程数是否已超过maximumPoolSize?没满,则创建一个新的工作线程来执行任务,满了,则交给拒绝策略来处理这个任务。
(我的理解:提交任务—>如果线程数未达到corePoolSize,则创建线程执行任务—>如果达到corePoolSize,仍让提交了任务,则会有任务等待,所以将任务保存在任务队列中,直到任务队列workQueue已满—>如果workQueue已满,仍然有任务提交,但未达到最大线程数,则继续创建线程执行任务,直到线程数达到maximumPoolSize,如果达到了maximumPoolSize,则根据饱和策略拒绝该任务。这也就解释了为什么有了corePoolSize还有maximumPoolSize的原因。)
  关于线程池的工作原理后期从源代码分析。

4 线程池的监控

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

  通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。

    /**
     * 给定的Thread执行Runnable之前调用此方法
     *
     * @param t the thread that will run task {@code r}
     * @param r the task that will be executed
     */
    protected void beforeExecute(Thread t, Runnable r) { }
    /**
     * 给定的Runnable完成后执行此方法
     * This method is invoked by the thread that executed the task. 
     * @param r the runnable that has completed
     * @param t the exception that caused termination, or null if
     * execution completed normally
     */
    protected void afterExecute(Runnable r, Throwable t) { }
    /**
     * 当Executor终止时调用此方法.  
     * 注意:方法中子类应调用super.terminated()
     */
    protected void terminated() { }

  例如:

class ExtendedExecutor extends ThreadPoolExecutor {
      // ...
      protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null && r instanceof Future>) {
          try {
            Object result = ((Future>) r).get();
          } catch (CancellationException ce) {
              t = ce;
          } catch (ExecutionException ee) {
              t = ee.getCause();
          } catch (InterruptedException ie) {
              Thread.currentThread().interrupt(); // ignore/reset
          }
        }
        if (t != null)
          System.out.println(t);
      }
}

  本文只是简单介绍了下线程池的基本使用和简单的处理流程,后期再深入源代码分析。
参考:
http://www.uml.org.cn/j2ee/201212193.asp
http://www.jiagou4.com/2015/07/726.html
http://blog.csdn.net/sayyanfu/article/details/8574098

  转载请注明出处:线程池的基本使用和执行流程分析

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