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

898 阅读7分钟

「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战

为什么使用线程池

在Java程序中,我们常常是在收到一个任务之后就创建一个线程去执行,但是如果任务比较多的时候,我们就需要频繁地创建和销毁线程,对资源消耗比较多,同时响应速度也比相对较慢。

为了避免频繁地创建和销毁线程而浪费资源,同时提高响应速度,我们可以使用线程池来优化。

线程池优势

线程池就是把线程放到一个“池子”里,需要执行任务的时候就从池子里选择一个线程来执行,使用线程池有以下优势:

  • 降低资源消耗:通过直接去池子里获取线程减少了线程频繁创建和销毁的开销;
  • 提高响应速度:任务到来时无需等待创建线程,直接从池子中取;
  • 提高线程可管理性:可以管理线程,避免出现创建过多线程,可以根据系统特性设置参数进行调优、监控。

ThreadPoolExecutor类

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,它有四个构造方法:

3a0868cb2f988d412ca717615426ba49.png

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

构造方法的相关参数详解

  • corePoolSize:核心线程数,线程池创建完成后,线程池中的线程数量为0,当有任务来时才创建新线程放到线程池中。
  • maximumPoolSize:最大线程数量,表示线程池最大可以容纳的线程数量,当线程数量达到corePoolSize且等待队列workQueue已经满时,如果还有新任务到来,还没达到maximumPoolSize时,会再创建线程放到线程池中。
  • keepAliveTime:线程空闲时存活时间
  • unit:枚举类型,keepAliveTime的单位,有以下几个枚举值
    • TimeUnit.DAYS; //天
    • TimeUnit.HOURS; //小时
    • TimeUnit.MINUTES; //分钟
    • TimeUnit.SECONDS; //秒
    • TimeUnit.MILLISECONDS; //毫秒
    • TimeUnit.MICROSECONDS; //微妙
    • TimeUnit.NANOSECONDS; //纳秒
  • workQueue:阻塞队列,用来暂存等待执行的任务,常用的选择有:
    • ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
    • LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
    • SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
    • PriorityBlockingQueue :支持优先级排序的无界阻塞队列
    • DelayQueue:使用优先级队列实现的无界阻塞队列。
  • threadFactory:创建线程的工厂,用于创建线程。
  • handler:拒绝策略,当线程池的任务队列达到maximumPoolSize时,新任务到来就会采取拒绝策略,有以下四种:
    • ThreadPoolExecutor.AbortPolicy:丢弃任务抛出异常;
    • ThreadPoolExecutor.DiscardPolicy:丢弃任务但不抛异常;
    • ThreadPoolExecutor.DiscardOldPolicy:每次新任务来时,丢弃等待队列最前面的任务;
    • ThreadPoolExecutor.CallerRunsPolicy: 任务创建线程执行。

在ThreadPoolExecutor类中有几个非常重要的方法

  • execute():提交任务交由线程池去执行。

  • submit():与execute()方法一样,向线程池提交一个任务,由线程池去执行。该方法声明与ExecutorService中,在AbstractExecutorService中进行了实现,在ThreadPoolExecutor中没有对其进行重写,直接调用父类的submit(),与execute()方法不同的是,它能返回任务执行的结果,使用Future来获取任务执行结果。

  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。

  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

线程池状态

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 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;

ctl包含两部分信息,运行状态和线程数量,高3位报错runState,低29位保存workCount。

线程池状态共有五种:

  • RUNNING:能接受新任务也能处理阻塞队列里的任务;
  • SHUTDOWN:调用shutdown()进入该状态,关闭状态,不再接受新任务,可以继续处理阻塞队列中已有的任务;
  • STOP:调用shutdownNow()进入该状态,不接受新任务也不处理任务,同时会中断正在处理的线程;
  • TIDYING:所有任务都已终止,workCount为0;
  • TERMINATED:死亡状态,在terminated()方法执行后进入该状态。

实例

创建一个线程池:

ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,20, 
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(10));

核心线程数为5,最大线程数为10,空闲存活时间为20秒,阻塞队列为LinkedBlockingQueue,大小为10。

创建一个任务类MyTask,实现了Runnable接口,重写run()方法。

class MyTask implements Runnable{
    private final int taskNum;
    public MyTask(int num){
        this.taskNum=num;
    }

    @Override
    public void run() {
        System.out.println("线程"+Thread.currentThread().getName()+"正在执行任务:"+taskNum);
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务"+taskNum+"执行完毕!");
    }
}

循环提交任务

for (int i=0;i<15;i++){
    MyTask task = new MyTask(i);
    executor.execute(task);
    System.out.println("线程池中线程数量:"+executor.getPoolSize()
            +", 队列中等待的的任务数量:" +executor.getQueue().size()
    +",已执行完毕的任务数目:"+executor.getCompletedTaskCount());
}

完整代码:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Test01 {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
                5,
                20,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(10));
        for (int i=0;i<15;i++){
            MyTask task = new MyTask(i);
            executor.execute(task);
            System.out.println("线程池中线程数量:"+executor.getPoolSize()
                    +", 队列中等待的的任务数量:" +executor.getQueue().size()
            +",已执行完毕的任务数目:"+executor.getCompletedTaskCount());
        }
        executor.shutdown();
    }
}
class MyTask implements Runnable{
    private final int taskNum;
    public MyTask(int num){
        this.taskNum=num;
    }

    @Override
    public void run() {
        System.out.println("线程"+Thread.currentThread().getName()+"正在执行任务:"+taskNum);
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务"+taskNum+"执行完毕!");
    }
}

运行结果:

线程pool-1-thread-1正在执行任务:0
线程池中线程数量:1, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程池中线程数量:2, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程池中线程数量:3, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-2正在执行任务:1
线程pool-1-thread-3正在执行任务:2
线程池中线程数量:4, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-4正在执行任务:3
线程池中线程数量:5, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-5正在执行任务:4
线程池中线程数量:5, 队列中等待的的任务数量:1,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:2,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:3,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:4,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:6,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:7,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:8,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:9,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:10,已执行完毕的任务数目:0
任务2执行完毕!
任务0执行完毕!
任务3执行完毕!
任务1执行完毕!
任务4执行完毕!
线程pool-1-thread-2正在执行任务:8
线程pool-1-thread-4正在执行任务:7
线程pool-1-thread-1正在执行任务:6
线程pool-1-thread-3正在执行任务:5
线程pool-1-thread-5正在执行任务:9
任务8执行完毕!
任务9执行完毕!
线程pool-1-thread-2正在执行任务:10
任务7执行完毕!
任务5执行完毕!
任务6执行完毕!
线程pool-1-thread-3正在执行任务:13
线程pool-1-thread-4正在执行任务:12
线程pool-1-thread-5正在执行任务:11
线程pool-1-thread-1正在执行任务:14
任务10执行完毕!
任务13执行完毕!
任务11执行完毕!
任务14执行完毕!
任务12执行完毕!

执行结果分析:

线程池核心线程数为5,所以任务0,1,2,3,4分别由线程池创建线程执行,任务5-14进入阻塞队列,阻塞队列中等待的任务数量为10,当任务0,1,2,3,4执行完毕,任务5,6,7,8,9依次由线程pool-1-thread-1—pool-1-thread-5执行,具体由哪一个线程顺序不确定,任务5-9执行完毕再执行阻塞队列中的任务10-14。

如果把阻塞队列大小改为5

ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,20, 
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(5));

执行结果:

线程pool-1-thread-1正在执行任务:0
线程池中线程数量:1, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程池中线程数量:2, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-2正在执行任务:1
线程池中线程数量:3, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-3正在执行任务:2
线程池中线程数量:4, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-4正在执行任务:3
线程池中线程数量:5, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-5正在执行任务:4
线程池中线程数量:5, 队列中等待的的任务数量:1,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:2,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:3,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:4,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程池中线程数量:6, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程池中线程数量:7, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程pool-1-thread-6正在执行任务:10
线程池中线程数量:8, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程pool-1-thread-7正在执行任务:11
线程池中线程数量:9, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程pool-1-thread-8正在执行任务:12
线程pool-1-thread-9正在执行任务:13
线程池中线程数量:10, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程pool-1-thread-10正在执行任务:14
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task testThread.MyTask@12a3a380 rejected from java.util.concurrent.ThreadPoolExecutor@29453f44[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at testThread.Test01.main(Test01.java:16)
任务13执行完毕!
任务3执行完毕!
任务2执行完毕!
任务1执行完毕!
任务12执行完毕!
任务11执行完毕!
任务10执行完毕!
任务14执行完毕!
任务0执行完毕!
任务4执行完毕!
线程pool-1-thread-8正在执行任务:9
线程pool-1-thread-2正在执行任务:8
线程pool-1-thread-4正在执行任务:7
线程pool-1-thread-3正在执行任务:6
线程pool-1-thread-9正在执行任务:5
任务8执行完毕!
任务9执行完毕!
任务7执行完毕!
任务6执行完毕!
任务5执行完毕!

执行结果分析:

我们可以看到,在线程池中线程数量为10, 队列中等待的的任务数量为5时,新任务来了就抛出异常了。

上面的执行结果中,当等待队列满了,核心线程数也满了,还未达到最大线程数时,新任务到来会先于等待队列执行。

默认的拒绝策略是AbortPolicy,抛出异常并拒绝接受新任务。 拒绝接受任务的时机是:线程数量等于maximumPoolSize且阻塞队列已满时。

四种常见的线程池

ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程。

  • Executors.newCacheThreadPool()

    缓存线程池,新来任务时,先看池子里是否有线程可以使用,如果有就使用池子里的,没有就新建线程到线程池中执行任务。

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
通过构造方法可以看到,核心线程数为0,代表如果没有任务,线程池中线程池最终会变为0,最大线程数为Integer.MAX_VALUE,线程空闲存活时间为60秒,使用SynchronousQueue队列,新来的任务不会保存到队列中。
  • Executors.newFixedThreadPool(int nThreads)

    固定线程数量的线程池,从构造方法可以看出线程池的线程数量固定为nThreads,没有空闲线程时任务进入阻塞队列等待。

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

    创建一个定长线程池,支持定时及周期性任务执行。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
public ScheduledThreadPoolExecutor(int corePoolSize) {
      super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
            new DelayedWorkQueue());
  }

延迟执行schedule():

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
        scheduledExecutorService.schedule(
          () -> System.out.println("延迟2秒后输出"),
          2,
          TimeUnit.SECONDS
          );
        scheduledExecutorService.shutdown();

定期执行 scheduleAtFixedRate():

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
        scheduledExecutorService.scheduleAtFixedRate(
          ()-> System.out.println("定期循环执行"),
          0,2,TimeUnit.SECONDS);

这里还有一个方法是定期执行:scheduleWithFixedDelay();

它也是延期执行,但是它们有区别:

scheduleAtFixedRate()是按照固定时间去执行,在前一次任务执行完后如果执行任务时间大于延迟执行时间,则立刻执行下一次任务。

scheduleWithFixedDelay()是在上一次任务执行完后再延迟固定的时间再执行下一次任务。

示例:

scheduleAtFixedRate():

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
        scheduledExecutorService.scheduleAtFixedRate(()-> {
            try {
                System.out.print("系统时间:"+ LocalDateTime.now().getMinute()+":"+LocalDateTime.now().getSecond());
                System.out.println(" 延期执行下一任务");
                Thread.sleep(6000);
                System.out.println("睡眠6秒,延迟5秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },0,5,TimeUnit.SECONDS);

执行结果:

可以看到上一次任务执行完后立刻执行下一次任务

f2d0cdc24269a8aa63e68c097b03f8f9.png

scheduleWithFixedDelay():

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.scheduleWithFixedDelay(()-> {
    try {
        System.out.print("系统时间:"+ LocalDateTime.now().getMinute()+":"+LocalDateTime.now().getSecond());
        System.out.println(" 延期执行下一任务");
        Thread.sleep(6000);
        System.out.println("睡眠6秒,延迟5秒");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
},0,5,TimeUnit.SECONDS);

执行结果:

6a312783569c5c2effcca61b17d0a389.png

可以看到在11秒后(上一任务完成后延迟5秒)才执行下一次任务。

  • Executors.newSingleThreadExecutor()

单身狗线程池,只有一个线程,线程空闲即销毁,所有任务顺序执行。

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

在使用newCacheThreadPoolnewFixedThreadPool线程池的时候有OOM的风险,如果创建的每个线程都一直运行,那么新任务会不断创建线程,jdk8默认一个线程大小为1MB,线程多了就会造成OOM,建议根据实际情况设置线程池参数。

动态参数设置

JDK原生线程池ThreadPoolExecutor提供了如下几个public的setter方法,可以动态设置线程池的参数。

794e0c6228fab87931f70fed9552a52b.png


参考资料