Java并发系列之中级篇之并发包:线程池ThreadPoolExecutor

版权声明:转载请注明链接 https://blog.csdn.net/weixin_38569499/article/details/87986941

目录

一、概览

1、背景

2、继承关系

二、线程池的内部实现

1、构造函数解析

1、int  corePoolSize

2、int  maximumPoolSize

3、long  keepAliveTime

4、TimeUnit  unit

5、BlockingQueue  workQueue

6、ThreadFactory  threadFactory

7、RejectedExecutionHandler  handler

2、线程池的任务调度逻辑

三、常用的线程池

1、newCachedThreadPool()

2、newFixedThreadPool(int nThreads)

3、newSingleThreadExecutor()

4、newSingleThreadScheduledExecutor()和计划任务线程池

5、newScheduledThreadPool(int nThreads)

四、常用方法介绍

1、任务提交:submit/execute

2、线程池的扩展:beforeExecute/afterExecute/terminated

3、线程池的关闭:shutdown/shutdownNow/isShutdown

4、线程池终止状态查询:isTerminating/isTerminated/awaitTermination

5、get方法:


一、概览

1、背景

    对于服务器应用,经常需要处理时间短但是数据庞大的请求。由于线程的创建和销毁会产生一定的开销,如果每次都为每个请求创建一个线程,执行完任务再去销毁,那么花在线程创建和销毁上的开销就会很大,导致系统的性能降低。

    JDK 1.5中增加了并发包java.util.concurrent,其中包含接口Executor及一系列的子接口和实现类,提供了对线程池的支持,用于在高并发的情况下对线程进行合理的复用,以降低线程创建和销毁带来的开销。

2、继承关系

    查看Executor的继承关系图谱可以看到:

    这里我们讨论的重心是ThreadPoolExecutor,这也就是我们平时说的“线程池”。

二、线程池的内部实现

1、构造函数解析

    ThreadPoolExecutor重载了多个构造函数,但是实际上其他构造函数都是在调用参数最多的构造函数。所以这里只是介绍一下参数最多的构造函数:

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

    以下会逐一介绍构造函数的参数,通过理解参数的作用来了解线程池的原理。

1、int  corePoolSize

    核心线程池大小,即线程池中保存的线程数。

2、int  maximumPoolSize

    最大线程池大小,即线程池中允许的最大线程数。

3、long  keepAliveTime

    多余线程的存活时长。当线程数量超过corePoolSize的时候,多余的线程如果连续空闲时间超过keepAliveTime指示的时长(时长的单位由下一个参数指定),那么该多余空闲线程就会被销毁,直到线程池中的线程数量达到corePoolSize为止。

4、TimeUnit  unit

    时间的单位,和上面的参数keepAliveTime结合使用,来指示多余线程的存活时长。

5、BlockingQueue<Runnable>  workQueue

1、概念

    等待队列,当线程池中的线程数量达到或者多于corePoolSize且无空闲线程的时候,如果这时候有新的任务需要执行,那么这个任务会放入到workQueue中,等待有空闲线程的时候再去执行。

2、BlockingQueue的实现类

    BlockingQueue接口有如下常见的实现类,可以用在ThreadPoolSize的实现类中:

1)直接提交的队列:SynchronousQueue。SynchronousQueue没有容量,每一个插入操作都要等待一个删除操作,每一个删除操作同样要等待一个插入操作。

    如果在这里使用SynchronousQueue,那么提交的任务实际上并不会保存,而是直接提交给线程池去执行。如果这时候没有空闲的线程,那么就会创建新的线程;如果已经达到最大线程数,那么就会触发拒绝策略。

2)有界的任务队列:ArrayBlockingQueue。ArrayBlockingQueue的构造函数必须带一个int型的容量参数,表示该队列的最大容量。

    当使用有界队列时,如果线程池的线程数已经达到了corePoolSize,那么任务就会提交到任务队列ArrayBlockingQueue中;当任务队列也满了之后,才会提交给线程池去创建新的线程。

3)无界的任务队列:LinkedBlockingQueue。与有界队列相比,除非系统资源耗尽,否则无界的队列不存在任务入队失败的情况。

    当使用无界队列时,如果线程池的线程数达到了corePoolSize,那么任务就会提交到任务队列LinkedBlockingQueue;但是因为LinkedBlockingQueue是无界的,永远不会满,所以线程池的线程数永远不会超过corePoolSize。

4)优先任务队列:PriorityBlockingQueue。PriorityBlockingQueue是带有执行优先级的队列,可以控制任务执行的先后。这是一个特殊的无界队列,跟其他队列最大的区别在于,其他队列ArrayBlockingQueue或者LinkedBlockingQueue都是按先进先出算法执行的,但是PriorityBlockingQueue可以根据任务自身的优先级顺序先后执行的。

6、ThreadFactory  threadFactory

    线程工厂,用于创建线程,可以定制化线程属性。一般使用默认的即可。

    线程池中的线程是通过ThreadFactory来创建的。ThreadFactory接口中只有一个方法,是用来创建新线程的:

Thread newThread(Runnable r);

    用户可以通过实现ThreadFactory并复写newThread方法,来自定义创建线程的逻辑,例如可以将线程设置为守护线程等。

7、RejectedExecutionHandler  handler

    拒绝策略。当等待队列已满,并且线程池中正在执行的线程数量已经达到maximumPoolSize的时候,如果有新的任务传入,这时候等待队列已经无法再存储新的任务,线程池也无法再创建新的线程去执行任务,那么这时候就会触发拒绝策略。

    RejectedExecutionHandler接口有4个实现类,作为4中不同的拒绝策略:

1、AbortPolicy:直接抛出异常,阻止系统正常工作。

2、CallerRunsPolicy:只要线程池没有关闭,该策略就会直接在调用者的线程中运行当前被丢弃的任务。这样虽然不会丢弃任务,但是任务提交线程的性能可能会受到很大影响。

3、DiscardOldestPolicy:丢弃等待队列中时间最久的一个任务,也就是即将被执行的一个任务,并尝试再次提交当前任务。

4、DiscardPolicy:丢弃当前任务,不做处理。

    如果上述JDK内置的策略无法满足需要,可以通过实现RejectedExecutionHandler接口来自定义拒绝策略。实现RejectedExecutionHandler接口需要实现接口方法:

/**
     * Method that may be invoked by a {@link ThreadPoolExecutor} when
     * {@link ThreadPoolExecutor#execute execute} cannot accept a
     * task.  This may occur when no more threads or queue slots are
     * available because their bounds would be exceeded, or upon
     * shutdown of the Executor.
     *
     * <p>In the absence of other alternatives, the method may throw
     * an unchecked {@link RejectedExecutionException}, which will be
     * propagated to the caller of {@code execute}.
     *
     * @param r the runnable task requested to be executed
     * @param executor the executor attempting to execute this task
     * @throws RejectedExecutionException if there is no remedy
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);

2、线程池的任务调度逻辑

    线程池ThreadPoolExecutor的任务调度逻辑如下:

三、常用的线程池

    JDK的java.util.concurrent包提供了一个工厂类Executors,可以用来创建几种常见的线程池:

// 1.
public static ExecutorService newCachedThreadPool()
// 2.
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
// 3.
public static ExecutorService newSingleThreadExecutor()
// 4.
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
// 5.
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)

1、newCachedThreadPool()

    无界线程池。这种线程池实际上就是corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,workQueue的类型是直接提交的线程队列SynchronousQueue。它的特点是线程池的大小没有上线,并且任务不会保存在任务队列,会直接提交给线程池去执行。

    当有新的任务提交的时候,如果有空闲的线程可以复用,就去复用空闲线程;如果没有空闲线程会立即创建线程去执行,并且不会有上限。缺点是当并发量特别高的时候,会导致开销很大。

2、newFixedThreadPool(int nThreads)

    固定大小线程池。这种线程池需要传入整型参数作为线程池中线程的数量,这种线程池的特点是corePoolSize = maximumPoolSize,所以线程池中的线程数是固定的,数量固定为函数的输入参数nThreads;同时它使用无界队列存储任务。

    当有新任务提交的时候,如果线程池中有空闲的线程,那么立即执行;如果没有,那么新的任务就会被暂存在一个任务队列中,等到有空闲线程再去按先入先出的顺序处理任务队列中的任务。

3、newSingleThreadExecutor()

    单一线程池,相当于nThreads=1的newFixedThreadPool,线程池中固定只有一个线程,实际上就是一条线程以队列的形式来执行任务。

4、newSingleThreadScheduledExecutor()和计划任务线程池

    方法返回一个ThreadScheduledExecutor的对象,线程池大小为1.ThreadScheduledExecutor接口在ThreadPoolExecutor接口的基础上扩展了在给定时间执行某项任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。

计划任务

    ThreadScheduledExecutor接口的实现类对象,并不会立即执行任务,而是在一定的延时后再去执行。接口定义了4个方法,用于延时执行任务:

public ScheduledFuture<?> schedule(Runnable command, 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);

1)schedule(Runnable command, long delay, TimeUnit unit):在延时delay指定的时长后执行command任务,任务只会执行一次;

2)schedule(Callable<V> callable, long delay, TimeUnit unit):在延时delay指定的时长后执行callable任务,任务只会执行一次;和上面方法不同的地方是,这里执行的任务是有返回值的Callable任务,而上面的Runnable任务没有返回值。

3)scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):以固定频率(FixedRate)周期性执行任务command,第一次执行任务是在initDelay指定的时延后,然后每隔period时长就会执行一次任务。

4)scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):以固定时延(FixedDelay)周期性执行任务command,第一次执行任务是在initDelay指定的时延后,然后每次执行完任务后间隔时长period就会重复执行一次。

FixedRate和FixedDelay的区别

1)FixedRate:指的是每次执行任务的开始时间是周期性的。例如周期是10s,任务执行耗时2s,第一次执行任务的时间点是10:00:00,那么10:00:02执行完第一次任务,第二次任务的开始时间是10:00:10。

2)FixedDelay:指的是下一次任务开始的时间,是上一层任务结束之后的特定时间之后。还是上面的例子,周期是10s,任务执行耗时2s,第一次执行任务的时间点是10:00:00,10:00:02执行完第一次任务,那么第二次任务的开始时间就变成了10:00:12。

    对于FixedRate有个需要注意的事情,如果任务执行的耗时比周期长,例如周期是10s,但是任务花费了12s才执行完,这时候并不会出现两个任务并行执行的情况,第二个任务会在第一个任务执行完成之后才会去执行。在上面的例子中,周期是10s,任务执行耗时12s,第一次执行任务的时间点是10:00:00,10:00:12执行完第一次任务,那么第二次任务的开始时间是10:00:12。

5、newScheduledThreadPool(int nThreads)

    方法返回一个ThreadScheduledExecutor的对象,线程池大小可以设置。

四、常用方法介绍

1、任务提交:submit/execute

// ExecutorService接口中定义的方法:
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);
// ThreadPoolExecutor中定义的方法:
public void execute(Runnable command)

    submit方法和execute方法都可以用来提交任务给线程池去执行,但是两者有一些区别,如下:

1、定义方法的类不同:submit是在ExecutorService接口中定义的,而execute方法是在ThreadPoolExecutor类中定义的。

2、返回值类型不同:execute方法返回值为空,submit方法会返回线程的直接结果。

3、对异常的处理方式不同

    如果执行的任务中产生了异常,execute方法会直接打印产生的异常的堆栈,由于该异常是在子线程中产生的,主线程中包围在execute方法周围的try-catch语句并不能捕获该异常。

    而submit方法提交的子线程如果产生了异常,当调用submit方法返回的Future实例的get方法时,可以在主线程中通过try-catch捕获该异常。这里需要注意的是,如果不调用Future示例的get方法,是不能捕获到异常的。

2、线程池的扩展:beforeExecute/afterExecute/terminated

protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }

    线程池中提供了三个空函数体的方法,用于线程池的扩展。顾名思义,beforeExecute方法用来在执行一个任务之前执行,afterExecute方法在执行一个任务之后执行,而terminated方法在线程池关闭执行执行。

    Runnable实现类:

class MyRunnable implements Runnable {
    private String name = "running_" + new Random().nextInt(100);

    public void run() {
        System.out.println("run");
    }

    public String getName() {
        return name;
    }
}

    主函数:

public class TestMain {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(5)) {
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("before execute: " + t.getName());
                System.out.println("before execute: " + ((MyRunnable) r).getName());
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.println("before execute: " + ((MyRunnable) r).getName());
            }

            @Override
            protected void terminated() {
                System.out.println("terminated");
            }
        };

        for (int i = 0; i < 5; i++) {
            Runnable runnable = new MyRunnable();
            pool.execute(runnable);
        }
        pool.shutdown();
    }
}

    执行结果:

before execute: pool-1-thread-1
before execute: pool-1-thread-3
run
before execute: pool-1-thread-2
run
before execute: running_23
run
before execute: pool-1-thread-3
run
before execute: running_66
before execute: running_1
before execute: running_72
before execute: pool-1-thread-2
run
before execute: running_41
terminated

3、线程池的关闭:shutdown/shutdownNow/isShutdown

    线程池使用完后,必须要进行关闭,否则无用的线程池会一直占据系统资源,导致内存泄漏的问题。

public void shutdown() 
public List<Runnable> shutdownNow()
public boolean isShutdown()

shutdown():使当前未执行完的线程继续执行,而不再添加新的任务。shutdown()方法不会阻塞线程,调用shutdown()方法后主线程中线程池的使命就马上结束了,而线程池会继续运行直到所有线程执行完才会停止。

shutdownNow():使当前线程池中不再添加新任务,同时会给线程池中正在执行的线程打上中断标志,即让正在执行的线程调用isInterrupted()方法的返回值是true。

isShutdown():判断当前线程池是否已经关闭。执行过shutdown或者shutdownNow方法的线程池就是已经关闭的。

4、线程池终止状态查询:isTerminating/isTerminated/awaitTermination

public boolean isTerminating()
public boolean isTerminated()
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException

isTerminating():线程池是否正在正在关闭。当线程池执行shutdown()或者shutdownNow()方法后,但是还有任务在执行、还没有彻底关闭的时候,方法返回true。

isTerminated():线程池是否已经关闭。当线程池shutdown()或者shutdownNow()方法后,并且所有任务都已经执行完成、线程池已经彻底关闭的时候,方法返回true。

    实际上,isShutdown()相当于(isTerminating() || isTerminated)。

 awaitTermination(long timeout, TimeUnit unit):等待timeout指示的时间长度之后,去查看线程池是否已经关闭。

5、get方法

    几个构造函数参数的get方法就不用说了,这里介绍几个其他的get函数:

public int getActiveCount()
public long getCompletedTaskCount()
public int getLargestPoolSize()
public int getPoolSize()
public long getTaskCount()

getActiveCount():获取线程池中正在执行的任务的约数。这里有两点需要注意,一是“正在执行的任务”并不包括等待队列中的任务;二是这里得到的结果是个约数,因为可能会已经统计过的任务在统计过程中执行完的情况。
getCompletedTaskCount():获取线程池中已经执行完成的任务的总数。这也是个约数。
getLargestPoolSize():获取线程池中出现过的、并行执行任务最多的时候,并行执行的任务数。
getPoolSize():获取当前线程池中的线程数。
getTaskCount():获取线程池中提交的任务数量的总数,包括已经执行完成的、正在执行的和等待执行的任务。这也是个约数。

猜你喜欢

转载自blog.csdn.net/weixin_38569499/article/details/87986941