Java 线程池之必懂应用-原理篇(下)

400 阅读9分钟

前言

线程并发系列文章:

Java 线程基础
Java 线程状态
Java “优雅”地中断线程-实践篇
Java “优雅”地中断线程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
线程池必懂系列

线程池系列文章:

Java 线程池之线程返回值
Java 线程池之必懂应用-原理篇(上)
Java 线程池之必懂应用-原理篇(下)

上篇文章分析了线程池的运行原理,本篇将重点分析如何使用线程池、线程池一些常用的API的使用。
通过本篇文章,你将了解到:

1、线程池的状态
2、线程池常用API
3、开启线程池的几种方式
4、如何关闭/停止线程池

1、线程池的状态

线程池有五种状态,分别为:

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

RUNNING--->运行状态,表示线程池正常运行。
SHUTDOWN--->关闭状态,表示线程池正在关闭中。
STOP--->停止状态,表示线程池正在停止中。
TIDYING--->整理状态,表示线程池正在整理中。
TERMINATED--->终止状态,表示线程池已终止。

五者转换如下:

在这里插入图片描述

其中:

shutdown() 表示停止空闲的线程。
shutdownNow() 表示停止所有线程,并清空等待队列。
tryTerminate() 判断当前状态是否满足流转到TIDYING 状态。
terminated() 空方法,可由子类实现做一些清理业务层的动作。

线程池的状态不可逆,从RUNNING 到TERMINATED 状态,越往后限制越严苛。
线程池创建时处在RUNNING 状态。
只有处在RUNNING 状态才会接受新的任务。
在SHUTDOWN 状态时,等待队列的里任务还可以被执行。 在STOP 状态时,表示清空了等待队列并且所有线程被终止。 TIDYING 状态时,表示线程池里已经没有线程。 TERMINATED 状态时,线程池已经彻底停止了。

2、线程池常用API

当构造了ThreadPoolExecutor 对象后,就可以使用其提供的对外暴露的方法,接下来列举几个常用的方法。

构造方法

共有四个构造方法,最终都调用到:

    /**
     * @param corePoolSize      核心线程个数
     * @param maximumPoolSize   最大线程个数
     * @param keepAliveTime     线程最久的空闲时间,超过该时间就会被回收
     * @param unit              keepAliveTime 的单位
     * @param workQueue         等待队列
     * @param threadFactory     线程工厂
     * @param handler           拒绝策略
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        //记录到成员变量里
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

其它方法

public void allowCoreThreadTimeOut(boolean value)
是否允许核心线程空闲一定的时间后被回收,value=true 表示允许,最终设置给成员变量:allowCoreThreadTimeOut。

public boolean awaitTermination(long timeout, TimeUnit unit)
阻塞等待线程池变为TERMINATED状态,timeout 为限时等待时间,unit为时间单位。

public void execute(Runnable command)
提交任务到线程池。

public int getActiveCount()
获取当前活跃(忙碌)的线程数,活跃与空闲是对立的,通过Worker里的isLocked()方法来判定是否活跃,isLocked()=true,表示占有锁,说明当前线程正在执行任务,否则是空闲的。

public long getCompletedTaskCount()
线程池执行完毕的任务个数。

public long getTaskCount()
线程池总的任务数(包含已完成和正在执行的)

public int getCorePoolSize()
核心线程个数。

public int getMaximumPoolSize()
线程池允许的最多线程个数。

public int getPoolSize()
线程池当前存活的线程个数

public int getLargestPoolSize()
线程池曾经开启的最大线程数。

public long getKeepAliveTime(TimeUnit unit)
获取线程池允许空闲的时间。

public BlockingQueue getQueue()
获取任务队列,通过任务队列即可知道当前排队的任务数。

public ThreadFactory getThreadFactory()
获取创建线程的工厂类。

public void shutdown()
关闭线程池。该方法只中断空闲的线程。

public List shutdownNow()
立即关闭线程池。中断所有线程,并清空任务队列。
shutdown()与shutdownNow() 稍后详细分析。

public void setCorePoolSize(int corePoolSize)
设置核心线程数。

public void setMaximumPoolSize(int maximumPoolSize)
设置最大线程数。

public void setKeepAliveTime(long time, TimeUnit unit)
设置线程最大空闲时间。

3、开启线程池的几种方式

基础开启方式

构造ThreadPoolExecutor 对象:

        //直接构造ThreadPoolExecutor 方式
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        for (int i = 0; i < 10; i++) {
            int ii = i;
            threadPoolExecutor.execute(() -> {
                System.out.println("runnable i=" + ii);
                System.out.println("active count:" + threadPoolExecutor.getActiveCount() + " pool size:" + threadPoolExecutor.getPoolSize());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

虽然线程池提供了几个重载的构造方法来使用线程池,但是构造方法入参比较多,不利于复用。线程池提供了Executors类来封装构造线程池对象。

在这里插入图片描述

接下来简单分析它们的原理及其使用场景。

newSingleThreadExecutor

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

此种方式创建的线程池只有1个核心线程,最大线程数为1,不设超时时间。
后续的任务到来后,将会被放到队列里,理论上这个队列是无限大的。

demo:

        ExecutorService singleExecutorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            int ii = i;
            singleExecutorService.execute(() -> {
                System.out.println("runnable i=" + ii);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

newFixedThreadPool

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

指定核心线程数和最大线程数,不设超时时间。
后续的任务到来后,将会被放到队列里,理论上这个队列是无限大的。
demo:

        ExecutorService fixedExecutorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 10; i++) {
            int ii = i;
            fixedExecutorService.execute(() -> {
                int activeCount = ((ThreadPoolExecutor)fixedExecutorService).getActiveCount();
                System.out.println("runnable i=" + ii + " activeCount:" + activeCount);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

newCachedThreadPool

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

核心线程数为0,最大线程数为Integer.MAX_VALUE(理论上开不了这么多线程),超时时间60秒,使用的是SynchronousQueue 队列。
SynchronousQueue 为无缓存队列,put和take需要配对使用。以生产者消费者为例,生产者生产东西,若没有消费者消费,则生产者阻塞,反之若是消费者没有东西可取,那么也阻塞。
与线程池配合使用时,流程如下:

1、初次提交任务,开启新线程执行任务。
2、若任务执行完毕,则线程阻塞等待新的任务到来。
3、新任务提交,则之前阻塞的线程被唤醒执行任务。
4、若第2步线程还未执行完成任务而又来了新的任务,那么将会开启新的线程执行任务。
5、当线程空闲时间超过60秒后将被回收。

demo:

        ExecutorService cacheExecutorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            int ii = i;
            cacheExecutorService.execute(() -> {
                int activeCount = ((ThreadPoolExecutor)cacheExecutorService).getActiveCount();
                System.out.println("runnable i=" + ii + " activeCount:" + activeCount);
            });
        }

在这里插入图片描述


从日志看出,短时间内创建了6个线程,因此使用此种方式创建线程池,存在短时间内创建大量线程的风险。

newScheduledThreadPool

    public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue(), threadFactory);
    }

指定核心线程数,最大线程数为Integer.MAX_VALUE(理论上开不了这么多线程),超时时间为10毫秒,使用DelayedWorkQueue 队列,顾名思义该队列属于延迟队列。
ScheduledThreadPoolExecutor 多用于定时执行任务的场景。
demo:

        ScheduledExecutorService scheduleExecutorService = Executors.newScheduledThreadPool(2);
        for (int i = 0; i < 10; i++) {
            int ii = i;
            scheduleExecutorService.schedule(() -> {
                int activeCount = ((ThreadPoolExecutor)scheduleExecutorService).getActiveCount();
                System.out.println("runnable i=" + ii + " activeCount:" + activeCount);
            }, 2, TimeUnit.SECONDS);
        }

newWorkStealingPool

此种方式是jdk1.8(含)之后引入的,属于抢占式任务线程池,和之前四种的实现方式差异较大。

demo:

        ExecutorService stealService = Executors.newWorkStealingPool();
        for (int i = 0; i < 10; i++) {
            int ii = i;
            stealService.execute(() -> {
                System.out.println("runnable i=" + ii);
            });
        }

几种创建方式的适用场景:

在这里插入图片描述

当然,如果现有的封装方式不满足业务需求,那么需要自己构造ThreadPoolExecutor 对象,此种方式虽然繁琐,但是自由度最大最能满足自定义的需求。

另外需要注意的是:

1、ExecutorService 里的方法比较少,若是想要获取线程池更多参数可以将ExecutorService 强转为ThreadPoolExecutor。
2、newSingleThreadExecutor 与newWorkStealingPool 返回的ExecutorService 不能强转为ThreadPoolExecutor。

4、如何关闭/停止线程池

之前列举常用的API时候有提到过两个方法:
shutdown() 与shutdownNow()。

shutdown()

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        //先上锁
        mainLock.lock();
        try {
            checkShutdownAccess();
            //修改状态为 SHUTDOWN
            advanceRunState(SHUTDOWN);
            //中断空闲的线程
            interruptIdleWorkers();
            onShutdown(); //子类可以设置钩子方法
        } finally {
            mainLock.unlock();
        }
        //尝试进入到TIDYING状态
        tryTerminate();
    }

    private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //遍历workers
            for (Worker w : workers) {
                Thread t = w.thread;
                //线程没有被中断并且能获取到锁
                if (!t.isInterrupted() && w.tryLock()) {
                    //能获取锁说明线程是空闲的
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

该方法只是中断空闲的线程,不影响正在执行任务的线程,也不影响队列里的任务。

shutdownNow()

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            //修改状态为 Stop
            advanceRunState(STOP);
            //中断所有线程
            interruptWorkers();
            //清空任务队列
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        //尝试进入到TIDYING状态
        tryTerminate();
        //返回被清空的任务队列
        return tasks;
    }

    private void interruptWorkers() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //中断所有线程
            for (Worker w : workers)
                w.interruptIfStarted();
        } finally {
            mainLock.unlock();
        }
    }

从方法名字可以看出,是立即停止线程池,那么必须要中断所有的线程。
此处你可能会有疑惑:Thread.interrupt()能中断没有阻塞的线程吗?
答案是:不能。
既然不能中断,那还有啥作用呢?
答案是:唤醒正在阻塞的线程并配合STOP与SHUTDOWN状态即可让线程结束运行。 具体分析请移步:Java “优雅”地中断线程(原理篇)

线程池并没有单独停止某个线程的功能,要么不停止,要么全部停止。线程池淡化了单个线程概念, 更加侧重于任务的调度。

演示代码 若是有帮助,给github 点个赞呗~

至此,线程池相关知识已分析完毕。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java