线程池学习

一、为什么要用线程池

Java高并发应用频繁创建和销毁线程的操作效率很低的,而且编程规范是不允许这样做的。如何降低Java线程的创建成本?必须使用到线程池。线程池主要解决了以下两个问题:

  • (1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建的线程进行复用,使得性能提升明显。
  • (2)线程管理:每个Java线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。

说明:
编程规范中不允许在应用中自行显式地创建线程,线程必须通过线程池提供。由于创建和销毁线程需要时间以及系统资源开销,使用线程池的好处是减少这些开销,解决资源不足的问题。

二、JUC的线程池架构

JUC就是java.util.concurrent工具包的简称,该工具包是从JDK 1.5开始加入JDK的,是用于完成高并发、处理多线程的一个工具包。

在多线程编程中,任务都是一些抽象且离散的工作单元,而线程是使任务异步执行的基本机制。随着应用的扩张,线程和任务管理也变得非常复杂。为了简化这些复杂的线程管理模式,我们需要一个“管理者”来统一管理线程及任务分配,这就是线程池。

在JUC中有关线程池的类与接口的架构图大致如下
JUC中线程池的类与接口的架构

1. Executor

Executor是Java异步目标任务的“执行者”接口,其目标是执行目标任务。“执行者”Executor提供了execute()接口来执行已提交的Runnable执行目标实例。Executor作为执行者的角色,其目的是提供一种将“任务提交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:

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

2. ExecutorService

ExecutorService继承于Executor。它是Java异步目标任务的“执行者服务接”口,对外提供异步任务的接收服务。ExecutorService提供了“接收异步任务并转交给执行者”的方法,如submit系列方法、invoke系列方法等,具体如下:

public interface ExecutorService extends Executor {
	//向线程池提交单个异步任务
     <T> Future<T> submit(Callable<T> task);
     //向线程池提交批量异步任务
     <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) 
     			throws InterruptedException;
}

3. AbstractExecutorService

AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。AbstractExecutorService存在的目的是为ExecutorService中的接口提供默认实现。

4. ThreadPoolExecutor

ThreadPoolExecutor继承于AbstractExecutorService抽象类。
ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。

5. ScheduledExecutorService

ScheduledExecutorService是一个接口,它继承于ExecutorService。它是一个可以完成“延时”和“周期性”任务的调度线程池接口,其功能和Timer/TimerTask类似。

6. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。
ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序中,ScheduledThreadPoolExecutor的性能要优于Timer。

7. Executors

Executors是一个静态工厂类,它通过静态工厂方法返回ExecutorService、ScheduledExecutorService等线程池示例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。

三、Executors的4种快捷创建线程池的方法

1. newSingleThreadExecutor创建“单线程化线程池”

该方法用于创建一个“单线程化线程池”,也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。
测试如下:

package com.cml.thread;

import java.util.concurrent.*;

/**
 * @author yys
 */
public class Main {

    public static void main(String[] args) {
        ExecutorService service = Executors.newSingleThreadExecutor();
        for (int j = 0; j < 3; j++) {
            service.execute(Main::run);
        }
        service.shutdown();
    }

    static int i;

    public static void run() {
        try {
            i ++;
            System.out.println("任务" + i + "开始运行");
            Thread.sleep(1000);
            System.out.println(i + " >> " + Thread.currentThread().getName());
            Thread.sleep(1000);
            System.out.println("任务" + i + "运行结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

任务1开始运行
1 >> pool-1-thread-1
任务1运行结束
任务2开始运行
2 >> pool-1-thread-1
任务2运行结束
任务3开始运行
3 >> pool-1-thread-1
任务3运行结束

从以上输出中可以看出,单线程线程池有以下特点:
(1)任务是按照提交的次序顺序执行的。
(2)池中的唯一线程的存活时间是无限的。
(3)当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。
总体来说,单线程化的线程池所适用的场景是:任务按照提交次序,一个任务一个任务地逐个执行的场景。
以上用例在最后调用shutdown()方法来关闭线程池。执行shutdown()方法后,线程池状态变为SHUTDOWN,此时线程池将拒绝新任务,不能再往线程池中添加新任务,否则会抛出RejectedExecutionException异常。此时,线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成才会退出。还有一个与shutdown()类似的方法,叫作shutdownNow(),执行shutdownNow()方法后,线程池状态会立刻变成STOP,并试图停止所有正在执行的线程,并且不再处理还在阻塞队列中等待的任务,会返回那些未执行的任务。

2. newFixedThreadPool创建“固定数量的线程池”

该方法用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。
测试如下:

package com.cml.thread;

import java.util.concurrent.*;

/**
 * @author yys
 */
public class Main {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(3);
        for (int j = 0; j < 10; j++) {
            service.execute(Main::run);
        }
        service.shutdown();
    }

    static int i;

    public static void run() {
        int j;
        try {
            synchronized (Main.class) {
                i ++;
                j = i;
            }
            System.out.println("任务" + j + "开始运行");
            Thread.sleep(1000);
            System.out.println(j + " >> " + Thread.currentThread().getName());
            Thread.sleep(1000);
            System.out.println("任务" + j + "运行结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

任务2开始运行
任务1开始运行
任务3开始运行
2 >> pool-1-thread-2
1 >> pool-1-thread-1
3 >> pool-1-thread-3
任务3运行结束
任务2运行结束
任务1运行结束
任务4开始运行
任务6开始运行
任务5开始运行
4 >> pool-1-thread-3
6 >> pool-1-thread-1
5 >> pool-1-thread-2
任务6运行结束
任务4运行结束
任务7开始运行
任务5运行结束
任务8开始运行
任务9开始运行
8 >> pool-1-thread-1
9 >> pool-1-thread-2
7 >> pool-1-thread-3
任务8运行结束
任务9运行结束
任务7运行结束
任务10开始运行
10 >> pool-1-thread-1
任务10运行结束

在测试用例中,创建了一个线程数为3的“固定数量线程池”,然后向其中提交了10个任务。从输出结果可以看到,该线程池同时只能执行3个任务,剩余的任务会排队等待。
“固定数量的线程池”的特点大致如下:
(1)如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
(2)线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
(3)在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。
“固定数量的线程池”的适用场景:需要任务长期执行的场景。“固定数量的线程池”的线程数能够比较稳定地保证一个数,能够避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能少地分配线程。
“固定数量的线程池”的弊端:内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。

3. newCachedThreadPool创建“可缓存线程池”

该方法用于创建一个“可缓存线程池”,如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。

测试如下:

package com.cml.thread;

import java.util.concurrent.*;

/**
 * @author yys
 */
public class Main {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        for (int j = 0; j < 10; j++) {
            service.execute(Main::run);
        }
        service.shutdown();
    }

    static int i;

    public static void run() {
        int j;
        try {
            synchronized (Main.class) {
                i ++;
                j = i;
            }
            System.out.println("任务" + j + "开始运行");
            Thread.sleep(1000);
            System.out.println(j + " >> " + Thread.currentThread().getName());
            Thread.sleep(1000);
            System.out.println("任务" + j + "运行结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

任务1开始运行
任务4开始运行
任务3开始运行
任务2开始运行
任务6开始运行
任务5开始运行
任务7开始运行
任务8开始运行
任务9开始运行
任务10开始运行
10 >> pool-1-thread-10
8 >> pool-1-thread-8
3 >> pool-1-thread-3
2 >> pool-1-thread-2
1 >> pool-1-thread-1
9 >> pool-1-thread-9
5 >> pool-1-thread-5
4 >> pool-1-thread-4
7 >> pool-1-thread-7
6 >> pool-1-thread-6
任务8运行结束
任务1运行结束
任务6运行结束
任务4运行结束
任务3运行结束
任务7运行结束
任务10运行结束
任务2运行结束
任务9运行结束
任务5运行结束

“可缓存线程池”的特点大致如下:
(1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
(2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
(3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。
“可缓存线程池”的适用场景:需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。
“可缓存线程池”的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。

4. newScheduledThreadPool创建“可调度线程池”

该方法用于创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。Executors提供了多个创建“可调度线程池”的工厂方法,部分如下:

 //方法一:创建一个可调度线程池,池内仅含有一个线程
public static ScheduledExecutorService newSingleThreadScheduledExecutor();
     
//方法二:创建一个可调度线程池,池内含有N个线程,N的值为输入参数corePoolSize
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) ;

newSingleThreadScheduledExecutor工厂方法所创建的仅含有一个线程的可调度线程池适用于调度串行化任务,也就是一个任务一个任务地串行化调度执行。

测试如下:

package com.cml.thread;

import java.util.concurrent.*;

/**
 * @author yys
 */
public class Main {

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        for (int j = 0; j < 2; j++) {
            //参数分别为   执行任务,首次执行延迟时间,每次执行间隔时间, 时间单位  SECONDS表示秒
            service.scheduleAtFixedRate(new MyTask(j), 0, 1, TimeUnit.SECONDS);
        }
        Thread.sleep(5000);
        service.shutdown();
    }

    static class MyTask implements Runnable {

        private int i;
        public MyTask(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            System.out.println("任务" + i + " >> " + Thread.currentThread().getName());
        }
    }

}

运行结果

任务1 >> pool-1-thread-2
任务0 >> pool-1-thread-1
任务1 >> pool-1-thread-2
任务0 >> pool-1-thread-1
任务1 >> pool-1-thread-1
任务0 >> pool-1-thread-2
任务1 >> pool-1-thread-1
任务0 >> pool-1-thread-2
任务0 >> pool-1-thread-2
任务1 >> pool-1-thread-1

newScheduledThreadPool工厂方法可以创建一个执行“延时”和“周期性”任务的可调度线程池,所创建的线程池为ScheduleExecutorService类型的实例。ScheduleExecutorService接口中有多个重要的接收被调目标任务的方法,其中scheduleAtFixedRate和scheduleWithFixedDelay使用得比较多。

ScheduleExecutorService中接收被调目标任务的方法之一scheduleAtFixedRate的定义如下:

public ScheduledFuture<?> scheduleAtFixedRate(
             Runnable command,      //异步任务target执行目标实例
             long initialDelay,     //首次执行延时
             long period,           //两次开始执行最小间隔时间
             TimeUnit unit                  //所设置的时间的计时单位,如TimeUnit.SECONDS常量
             );

ScheduleExecutorService中接收被调目标任务的方法之二scheduleWithFixedDelay的定义如下:

 public ScheduledFuture<?> scheduleWithFixedDelay(
         Runnable command,  //异步任务target执行目标实例
         long initialDelay,         //首次执行延时
         long delay,          //前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间)
         TimeUnit unit        //所设置的时间的计时单位,如TimeUnit.SECONDS常量
     );

当被调任务的执行时间大于指定的间隔时间时,ScheduleExecutorService并不会创建一个新的线程去并发执行这个任务,而是等待前一次调度执行完毕。

四、线程池的标准创建方式

大部分企业的开发规范都会禁止使用快捷线程池(具体原因稍后介绍),要求通过标准构造器ThreadPoolExecutor去构造工作线程池。Executors工厂类中创建线程池的快捷工厂方法实际上是调用ThreadPoolExecutor(定时任务使用ScheduledThreadPoolExecutor)线程池的构造方法完成的。ThreadPoolExecutor构造方法有多个重载版本,其中一个比较重要的构造器如下:

// 使用标准构造器构造一个普通的线程池
public ThreadPoolExecutor(
    int corePoolSize,            // 核心线程数,即使线程空闲(Idle),也不会回收
    int maximumPoolSize,                 // 线程数的上限
    long keepAliveTime, TimeUnit unit,   // 线程最大空闲(Idle)时长 
    BlockingQueue<Runnable> workQueue,     // 任务的排队队列
    ThreadFactory threadFactory,        // 新线程的产生方式
    RejectedExecutionHandler handler)    // 拒绝策略

参数的具体介绍如下:

  1. 核心和最大线程数量
    参数corePoolSize用于设置核心(Core)线程池数量,参数maximumPoolSize用于设置最大线程数量。线程池执行器将会根据corePoolSize和maximumPoolSize自动维护线程池中的工作线程,大致规则为:
    (1)当在线程池接收到新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到corePoolSize。
    (2)如果当前工作线程数多于corePoolSize数量,但小于maximumPoolSize数量,那么仅当任务排队队列已满时才会创建新线程。通过设置corePoolSize和maximumPoolSize相同,可以创建一个固定大小的线程池。
    (3)当maximumPoolSize被设置为无界值(如Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。
    (4)corePoolSize和maximumPoolSize不仅能在线程池构造时设置,也可以调用setCorePoolSize()和setMaximumPoolSize()两个方法进行动态更改。
  2. BlockingQueue
    BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,那么所接收到的目标任务缓存在阻塞队列中。
  3. keepAliveTime
    线程构造器的keepAliveTime(空闲线程存活时间)参数用于设置池内线程最大Idle(空闲)时长(或者说保活时长),如果超过这个时间,默认情况下Idle、非Core线程会被回收。

如果池在使用过程中提交任务的频率变高,也可以调用方法setKeepAliveTime(long,TimeUnit)进行线程存活时间的动态调整,可以将时长延长。如果需要防止Idle线程被终止,可以将Idle时间设置为无限大,具体如下:

 setKeepAliveTime(Long.MAX_VALUE,TimeUnit.NANOSECONDS);

默认情况下,Idle超时策略仅适用于存在超过corePoolSize线程的情况。但若调用了allowCoreThreadTimeOut(boolean)方法,并且传入了参数true,则keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程。

五、向线程池提交任务的两种方式

向线程池提交任务的两种方式大致如下:

  • 方式一:调用execute()方法
//Executor 接口中的方法
void execute(Runnable command);
  • 方式二:调用submit()方法
//ExecutorService 接口中的方法
<T> Future<T> submit(Callable<T> task); 
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

以上的submit()和execute()两类方法的区别在哪里呢?大致有以下三点:

  • (1)二者所接收的参数不一样
    Execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable、Runnable两种类型的参数。Callable类型的任务是可以返回执行结果的,而Runnable类型的任务不可以返回执行结果。
    Callable是JDK 1.5加入的执行目标接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。Runnable和Callable的主要区别为:Callable允许有返回值,Runnable不允许有返回值;Runnable不允许抛出异常,Callable允许抛出异常。
  • (2)submit()提交任务后会有返回值,而execute()没有
    execute()方法主要用于启动任务的执行,而任务的执行结果和可能的异常调用者并不关心。submit()方法也用于启动任务的执行,但是启动之后会返回Future对象,代表一个异步执行实例,可以通过该异步执行实例去获取结果。
  • (3)submit()方便Exception处理
    execute()方法在启动任务执行后,任务执行过程中可能发生的异常调用者并不关心。而通过submit()方法返回的Future对象(异步执行实例),可以进行异步执行过程中的异常捕获。

六、线程池的任务调度流程

线程池的任务调度流程(包含接收新任务和执行下一个任务)大致如下:

  • (1)如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
  • (2)如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。
  • (3)当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
  • (4)在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
  • (5)在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。
    在这里插入图片描述
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程夜游神

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值