Java并发编程:JUC之线程池的使用

JSR及JUC

什么是JSR

JSR,全称 Java Specification Requests, 即Java规范提案, 主要是用于向JCP(Java Community Process)提出新增标准化技术规范的正式请求。每次JAVA版本更新都会有对应的JSR更新,比如在Java 8版本中,其新特性Lambda表达式对应的是JSR 335,新的日期和时间API对应的是JSR 310。

什么是JSR 166

它是一个关于Java并发编程的规范提案,在JDK中,该规范由java.util.concurrent包实现,是在JDK 5.0的时候被引入的;

另外JDK6引入Deques、Navigable collections,对应的是JSR 166x,JDK7引入fork-join框架,用于并行执行任务,对应的是JSR 166y。

什么是JUC

即java.util.concurrent的缩写,该包参考自EDU.oswego.cs.dl.util.concurrent,是JSR 166标准规范的一个实现。

JUC的结构

java-thread-juc-overview.png

主要包含:

  • Locks:锁包
  • Tools:工具类
  • Collections:并发集合
  • Executor: 线程池
  • Atomic :原子类

Executors 线程池

Java5之前创建线程的方式:

  • 继承Thread类

  • 实现Runnable接口

  • 实现Callable接口

通过以上方式我们可以十分简便的创建线程,但也存在如下弊端

  1. 每次new Thread都会新建一个线程,这样频繁创建会大大降低系统性能,因为创建、销毁线程均需要时间。
  2. 线程缺乏统一管理,可无限制的创建线程,可能会占用过多的系统资源导致宕机或者OOM。
  3. 功能单一,缺乏定时执行、中断线程,合理控制线程最大并发数等功能。

Java5开始,自带的Executor框架,为开发者们自定义线程池带来了极大的便利,使用线程池的好处如下:

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

类结构图

java-thread-juc-futuretask.png

Executor

Executor是线程池的顶层接口,它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型。从字面意思可以理解,就是用来执行传进去的任务。

ExecutorService

ExecutorService继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等。

ScheduledExecutorService

ScheduledExecutorService继承自ExecutorService接口,可在给定的延迟后运行或定期执行的命令。

AbstractExecutorService

AbstractExecutorService继承自ExecutorService接口,基本实现了ExecutorService中声明的所有方法。此类使用 newTaskFor 返回的 RunnableFuture 实现 submit、invokeAny 和 invokeAll 方法,默认情况下,RunnableFuture 是此包中提供的 FutureTask 类。

Future

Future 表示了一个任务的生命周期,是一个可取消的异步运算,可以把它看作是一个异步操作的结果的占位符,它将在未来的某个时刻完成,并提供对其结果的访问。在并发包中许多异步任务类都继承自Future,其中最典型的就是 FutureTask。

FutureTask

FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证。

FutureTask类关系

java-thread-juc-futuretask.png

可以看到,FutureTask实现了RunnableFuture接口,RunnableFuture接口则继承了Runnable接口和Future接口,所以FutureTask既能当做一个Runnable直接被Thread执行,也能作为Future用来得到Callable的任务执行结果。

ThreadPoolExecutor

ThreadPoolExecutor是线程池中最核心的一个类,它继承了AbstractExecutorService。

构造函数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }

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

ThreadPoolExecutor类中提供了以上四个构造函数,通过观察可以发现,前三个构造函数最终还是调用第四个构造函数完成实例化。只不过前三个构造函数在调用第四个构造函数时,对部分参数赋予了默认值。

参数描述

  • corePoolSize:核心池大小,在创建完线程池后,线程池中是没有任何线程的,只有当任务来了之后,才会创建新线程去处理任务。除非调用prestartCoreThread()或者prestartAllCoreThreads()方法,线程池会提前创建并启动一个或corePoolSize个线程。当已创建的线程数大于corePoolSize后,任务将被放入任务队列中。

  • maximumPoolSize:线程池允许创建的最大线程数,当任务队列已经放满了、且已创建线程数小于maximunPoolSize时,则线程池会创建新的线程执行任务。如果使用了无界的任务队列,这个参数就没什么效果。

  • keepAliveTime:空闲线程存活时间,只有当已创建的线程数大于corePoolSize时,此参数才会起作用。即当线程池的线程数大于corePoolSize时,如果一个线程的空闲时间超过keepAliveTime,就会销毁该线程,但是如果调用了allowCoreThreadTimeOut(boolean)方法,那么在线程池数量不大于corePoolSize时,此参数也会生效,直到池中的数量为0。如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。

  • unit:参数keepAliveTime的时间单位,有七种数值,在TimeUnit中有有七个静态属性。

    • NANOSECONDS(纳秒)
    • MICROSECONDS(微秒)
    • MILLISECONDS(毫秒)
    • SECONDS(秒)
    • MINUTES(分钟)
    • HOURS(小时)
    • DAYS(天)
  • workQueue:任务队列,用来存储等待执行的任务,有如下几种队列模式。

    • ArrayBlockingQueue:基于数组的有界阻塞队列,此队列按FIFO原则对元素进行排序。
    • LinkedBlockingQueue:基于链表的有界阻塞队列,此队列按FIFO原则对元素进行排序,吞吐量通常要高于ArrayBlockingQueue。
    • SynchronousQueue:是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。例如,t1线程往队列中插入元素时,必须等到t2线程中将队列中的元素取出才可插入,否则t1线程将一直处于阻塞状态。使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界(Integer.MAX_VALUE),避免线程拒绝执行操作。
    • PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
  • threadFactory:线程工厂,用于创建线程,一般情况下用默认的即可。但阿里开发手册建议我们通过线程工厂来设置线程名,方便出现问题时溯源。可以通过Google的ThreadFactoryBuilder实例化线程工厂。

    // 线程工厂,自定义线程名
    ThreadFactory namedFactory = new ThreadFactoryBuilder().setNameFormat("doc-detail-contact-%d").build();
    ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(10), namedFactory);
    
  • handler:拒绝处理任务的策略。当线程池和任务队列都满了,必须采取一种策略来处理提交的新任务,默认是AbortPolicy。

    • JDK内置4种线程池拒绝策略,如下
    策略类型 功能 使用场景
    CallerRunsPolicy(调用者运行策略) 当触发拒绝策略时,只要线程池没有关闭,就由提交任务的当前线程处理 一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了。
    AbortPolicy(中止策略) 当触发拒绝策略时,直接抛出拒绝执行的异常,中止策略的意思也就是打断当前执行流程 这个就没有特殊的场景了,但是一点要正确处理抛出的异常。ThreadPoolExecutor中默认的策略就是AbortPolicy,ExecutorService接口的系列ThreadPoolExecutor因为都没有显示的设置拒绝策略,所以默认的都是这个。但是请注意,ExecutorService中的线程池实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。当自己自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程。
    DiscardPolicy(丢弃策略) 直接静悄悄的丢弃这个任务,不触发任何动作 使用场景:如果你提交的任务无关紧要,你就可以使用它 。因为它就是个空实现,会悄无声息的吞噬你的的任务。所以这个策略基本上不用了
    DiscardOldestPolicy(弃老策略) 如果线程池未关闭,就弹出队列头部的元素,然后尝试执行 这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,我能想到的场景就是,发布消息,和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较
    • 第三方实现的拒绝策略:Dubbo、Netty、ActiveMQ、PinPoint等均有各自实现的拒绝策略,有兴趣可自行了解。

常用方法

  • shutDown():线程池将不再接收新的任务,当正在执行的任务以及队列中的任务全部执行完成后,将关闭线程池。
  • showDownnNow():线程池状态立马变为stop状态,并试图停止所有正在执行的任务。不再处理工作队列中的任务,并返回未被执行的任务集合。
  • isShowDown():判断线程池是否已关闭。
  • isTerminating():是否正在关闭线程池,但尚未完全终止。
  • isTerminated():线程池是否已经关闭。
  • awaitTermination(long timeout, TimeUnit unit):查看在指定时间内,线程池是否关闭,一般配合showDown()使用
  • prestartAllCoreThread():预创建corePoolSize个线程。
  • prestartCoreThread():每次调用创建一个核心线程。
  • remove(Runnable task):移除尚未被执行的任务
  • getActiveCount():获取正在执行任务的线程数。
  • getPoolSize():线程池中总共有多少个线程,包含正在执行任务的线程和休眠的线程。
  • getCompletedTaskCount():获取当前线程池中所有线程已完成的任务总数。
  • getTaskCount():获取线程池中要执行的任务数,包含运行中的和队列中的任务。
  • getCoreSize():获取构造函数中传入的corePoolSize()参数值。
  • getMaximumPoolSize():获取构造函数中传入的maximumPoolSize参数值。

Executors

Executors是创建线程池的工厂类,其中定义了许多静态方法,供开发者根据不同场景简便的创建线程池。

常用方法有以下四种:

/* 
 * 该方法返回一个固定线程数量的线程池,该线程池池中的线程数量始终不变。
 */
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);

/* 
 * 方法返回一个只有一个线程的线程池。
 */
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

/* 
 * 方法返回一个可根据实际情况调整线程数量的线程池。
 */
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

/* 
 * 返回一个ScheduledExecutorService对象,线程池大小为1。。
 */
ExecutorService newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();


/* 
 *  该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量
 */
ExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(1);

以上的工具类的具体实现都是基于ThreadPoolExecutor类,处理策略都是AbortPolicy(直接抛出异常)。

在使用线程池时,推荐使用ThreadPoolExecutor,根据自己的使用场景,合理的调整 corePoolSize,maximumPoolSize,workQueue,RejectedExecutionHandler,规避资源耗尽的风险。

结语

最后,如果你觉得这篇文章写的还不错的话,求赞,求收藏,求转发。更重要的是点一个大大的关注,大家的支持是我写博客的最大动力。如有不同意见,欢迎大家评论区留言,共同探讨。

你可能感兴趣的:(Java并发编程:JUC之线程池的使用)