Java并发工具学习(一)——线程池

前言

前面针对Java多线程基础涉及到的内容,几乎都做了总结,从这篇博客开始,我们总结一下Java并发工具的内容,从线程池开始

为什么要有线程池

主要是因为两点,1、反复创建线程开销很大(这个实在不想解释了);2、过多的线程会占用太多的内存

为了解决这两个问题,于是Java引入了池的概念。

用少量的线程来避免内存占用过多带来的麻烦;始终只让部分线程保持工作,且可以反复执行提交上来的任务,这就避免了频繁的线程切换带来的损耗。

线程池可以一定程度上加快响应速度,可以合理利用CPU和内存,同时也便于统一管理资源。在实际开发中,如果需要创建5个以上的线程,我们一般就要考虑用线程池来实现了。

线程池的参数与任务添加规则

相比创建单一的线程来说,创建线程池稍显麻烦了些,参数更多,同时还涉及更多的概念。一步步总结吧

线程池构造函数的参数

先需要熟悉一下创建线程池的参数,大概扫一下即可,有些可能不太知道意思。

参数名称 类型 含义
corePoolSize int 核心线程数
maxPoolSize int 最大线程数
keepAliveTime long 线程存活时间
workQueue BlockingQueue 存储任务的队列
threadFactory ThreadFactory 生成新线程的工厂类
handler RejectedExecutionHandler 线程池的拒绝策略

corePoolSize是核心线程数,线程池在完成初始化之后,线程池中是没有任何线程的,线程池会等待有任务到来的时候再创建线程(从任务队列中取出任务,让后创建线程)。

maxPoolSize最大线程数,线程池的优势之一就是可以根据任务的多少,做到灵活扩展自己的活动线程数。同一个线程池不同时间所需要执行的任务数也是不同的,在核心线程数不足以处理过多的任务数,这个时候我们就需要扩展线程池中处理任务的线程

在这里插入图片描述

keepAliveTime如果线程池当前的线程数多于corePoolSize,那么如果多余的线程空闲时间超过keepAliveTime,他们就会被终止

workQueue存储任务的队列,通常有三种存储任务的队列:1、直接交接的任务队列——SynchronousQueue;2、无界队列——LinkedBlockingQueue;3、有界的队列——ArrayBlockingQueue

threadFactory新的线程是由ThreadFactory创建的,默认使用Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级,并且都不是守护线程。如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等属性,不过通常情况下也没有这个必要。

handler拒绝策略,当线程池达到最大个数且任务队列满,这时候新提交的任务就会被拒绝,具体的拒绝操作就由拒绝策略来执行

添加任务的规则

如果单独记忆创建线程池的这些参数,其实并没有太大意义,我们需要属性并理解一下线程池添加任务的规则,就可以将这些参数串联起来

线程池由于是灵活扩展的,因此其添加任务也有一定的规则

1、如果当前线程池中的线程数小于corePoolSize,即使其他工作线程处于空闲状态,这个时候线程池依旧会创建一个线程来运行新的任务

2、如果线程数等于(或者大于)corePoolSize,但是小于maxPoolSize,则先将任务放入到队列,线程池中的活动线程个数为corePoolSize的值

3、如果队列这个时候都满了,并且线程池中的线程数小于maxPoolSize,则线程池创建一个新的线程来运行任务

4、如果队列满了,同时线程池中的线程个数已经达到了maxPoolSize,如果这个时候外部还有任务提交到线程池,则线程池直接执行拒绝策略(handler)

5、如果线程池中任务个数不多,这个时候存在很多活跃但空闲的线程,则这些空闲线程空闲的时间超过设置的keepAliveTime,则会被终止。

一张图表述线程池添加任务的规则

在这里插入图片描述

可以看出,判断顺序依次是corePoolSize->workQueue->maxPoolSize。

举一个简单的实例:

如果一个线程池核心线程数设置为5,最大线程数为10,队列长度为100。

刚开始,线程任务不多,来了三个任务,线程池就创建3个线程处理提交上来的3个任务。后面任务继续被添加到线程池,线程池中依旧先用5个核心线程处理这些任务,如果处理不了的,就暂时存放在任务队列中,如果随着时间的推移,任务队列中已经存满,后续依旧有任务提交上来,这个时候线程池就继续创建更多的线程来处理任务,如果活动线程个数超过设置的最大线程数10,则线程池就拒绝处理后续提交上来的任务。

根据线程池处理任务的规则,进一步总结一下:

1、如果通过设置corePoolSize和maxPoolSize相同,我们是否就可以创建固定活动线程个数的线程池。

2、线程池总体上是希望保持较少的活动线程个数,只有在负载(任务队列中的任务个数)变的很大的时候,才新开线程。

3、如果maxPoolSize设置为很高的值(比如Integer.MAX_VALUE),则理论上,线程池可以新增很多的线程来处理任务。

4、只有任务队列满的时候,线程池才会创建更多的活动线程,如果我们使用的是无界队列(比如LinkedBlockingQueue)来存储任务,也就是说任务队列永远不可能被存满,那么这个时候线程池中的活动线程数不可能会超过corePoolSize

所以根据线程池增减线程的特点,JDK为我们自定义了一些线程的线程池,这些线程池各有特点。

JDK提供的一些线程池

JDK其实给我们提供了一些常用的线程池,这些线程池各有特点,先进行一些梳理

newFixedThreadPool

其构造函数源码如下

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    
    
    //第一个参数是corePoolSize,第二个参数是maxPoolSize,第三个和第四个参数是keepAliveTime和它的单位
    //第四个参数是任务队列,第五个参数是ThreadFactory
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

可以看到,这是一个corePoolSize和maxPoolSize等大小的线程池,同时其任务队列又是一个无界的任务队列。因此任务过多的时候这个线程池中线程个数是固定的,故而称为FixedThreadPool。

基本使用实例

/**
 * autor:liman
 * createtime:2021-11-01
 * comment: newFixThreadPool的实例
 */
public class NewFixThreadPoolDemo {
    
    

    public static void main(String[] args) {
    
    
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for(int i=0;i<1000;i++){
    
    
            executorService.execute(new Task());
        }
    }

}

class Task implements Runnable {
    
    

    @Override
    public void run() {
    
    
        try {
    
    
            Thread.sleep(100);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"运行结束");
    }
}

由于任务过多的时候,这个线程池中的活动线程数是固定的,同时任务队列是没有容量上限的,所以当请求越来越多,并且无法及时处理完毕的时候,请求堆积时,容易造成占用大量的内存,可能会导致OOM

比如如下代码,将JVM参数设置的小一点,就会出现OOM的异常

/**
 * autor:liman
 * createtime:2021-11-01
 * comment: newFixedThreadPool出现OOM的实例
 * JVM 参数 -Xmx8m -Xms8m
 */
public class FixThreadPoolOOM {
    
    

    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) {
    
    
      for(int i=0;i<Integer.MAX_VALUE;i++){
    
    
          executorService.execute(new OOMTask());
      }
    }

}

class OOMTask implements Runnable{
    
    

    @Override
    public void run() {
    
    
        try {
    
    
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

newSingleThreadExecutor

构造函数源码

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    
    
    //可以看到只是在newFixedThreadPool的基础上,corePoolSize和maxPoolSize参数设置为1了
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

可以看到只是在newFixedThreadPool的基础上,corePoolSize和maxPoolSize参数设置为1了,newFixedThreadPool有的问题,这个线程池都有,可以简单理解为低配版本的newFixedThreadPool

newCachedThreadPool

可以缓存的线程池,是一个无边界的线程池,且具有自动回收多余线程的功能。其构造函数源码如下

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    
    
    //corePoolSize设置为0,maxPoolSize设置的为Integer.MAX_VALUE
    //任务队列用的是SynchronousQueue
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

构造函数中可以看出,其corePoolSize设置为0,maxPoolSize设置为整型的最大值,任务队列用的是SynchronousQueue,这个队列我们前面提到过,是一个直接交接的队列,其内部容量是0。任务队列中并不实质存储任务,而是直接交给线程去执行,因此线程池需要不断创建线程来处理提交过来的任务。
在这里插入图片描述

过一段时间之后,那些没有任务的空闲线程,会被回收。

实例代码

/**
 * autor:liman
 * createtime:2021-11-01
 * comment:
 */
public class CacheThreadPoolDemo {
    
    

    public static void main(String[] args) {
    
    
        ExecutorService executorService = Executors.newCachedThreadPool();
        for(int i=0;i<1000;i++){
    
    
            executorService.execute(new CacheThreadTask());
        }
    }
}

class CacheThreadTask implements Runnable{
    
    

    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName());
    }
}

不同于其他线程池,这里处理任务的线程个数有点多

在这里插入图片描述

这个线程池由于每次来了任务就新建线程,这一定程度上也会出现OOM

newScheduledThreadPool

支持定时或者周期性执行任务的线程池

其中一个构造函数源码

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    
    
    //大同小异,不想再介绍了
    //任务队列使用的是延时队列
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
}

基本使用实例

/**
 * autor:liman
 * createtime:2021-11-01
 * comment:scheduledThreadPool的实例
 */
public class ScheduledThreadPoolDemo {
    
    

    public static void main(String[] args) {
    
    
        ScheduledExecutorService executorService
                = Executors.newScheduledThreadPool(10);
        //延迟五秒执行指定的任务
        //executorService.schedule(new ScheduledTask(),5, TimeUnit.SECONDS);
        //刚开始第一次,1秒后执行任务,之后每隔3秒钟再执行一次
        executorService.scheduleAtFixedRate(new ScheduledTask(),1,3,TimeUnit.SECONDS);
    }
}
class ScheduledTask implements Runnable{
    
    

    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName());
    }
}

在JDK1.8中还新增了一些其他线程池,但是并不是特别常用,这里就不做总结记录了。

自动创建还是手动创建

关于线程池是自动创建还是手动创建,其实走到这里,各位应该有了答案,JDK虽然提供了一些可用的线程池,但是这些线程池都有一些缺陷,都被高度设计化了,如果我们业务场景比较契合这种自带的线程池,其实可以尝试,但是大多数情况下还是建议我们手动创建线程池。

如何设定线程池中的线程数量

1、如果我们的程序是CPU密集型的,比如加密和频繁的hash运算,这个时候最佳的线程数为CPU核心数的1-2倍

2、如果我们的程序是IO型的,比如频繁的读写文件,频繁的网络传输等,这个时候最佳线程数一般会大于CPU核心数很多倍,以JVM线程监控显示的繁忙情况为依据,保证线程空闲可以衔接上。

通常的计算公式为:
线 程 数 = C P U 核 心 数 ∗ ( 1 + 平 均 等 待 时 间 / 平 均 工 作 时 间 ) 线程数=CPU核心数*(1+平均等待时间/平均工作时间) 线=CPU(1+/)
所谓的平均等待时间只的是线程等待数据的耗时,平均工作时间就是线程处理数据的耗时。

正确停止线程池

线程池的停止比我们正常停止单个线程要来的简单。

相关的方法

shutdown

直接调用线程池的shutdown方法,这个时候相当于只是告知线程池一个中断信号,线程池会将队列中等待的任务,活动线程中正在执行的任务都执行完成,才进行停止。但是在线程池响应中断信号的这段时间,提交任务给线程池是会报错的。

实例代码

/**
 * autor:liman
 * createtime:2021-11-02
 * comment:
 */
public class ShutDownDemo {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
    
    
            fixedThreadPool.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        //这里调用shutdown ,可以看到线程池依旧不紧不慢的处理完自己的任务,然后再停止
        fixedThreadPool.shutdown();
        fixedThreadPool.execute(new ShutDownTask());//这里会抛出异常,处理中断的时候线程池不接受新的任务
    }

}

class ShutDownTask implements Runnable {
    
    

    @Override
    public void run() {
    
    
        try {
    
    
            Thread.sleep(500);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

isShutdown

判断线程池吃否在处理中断。

isTerminated

判断线程池是否完全停止

awaitTermination

判断线程池一段时间之后是否完全停止,如果完全停止则返回true,否则返回false

shutDownNow

立即关闭,比较暴力直接关闭正在执行的任务。

如果我们要正常停止线程池,则建议利用相关判断进行停止,而不是用最后一种方法直接暴力停止线程池。

拒接策略

拒绝策略这里不做过多实例演示,只是总结一下有几种拒绝策略

拒绝策略 拒绝方式
AbortPolicy 直接抛出异常,不会让其提交成功
DiscardPolicy 丢弃,线程池默默丢弃任务
DiscardOldestPolicy 丢弃队列中存在时间最久的任务
CallerRunsPolicy 提交任务的线程来执行拒绝策略

当然也支持开发者自定义拒绝策略,只需要实现对应拒绝策略的接口即可

线程池原理和源码简析

其实总结到这里,几乎涵盖了线程池中常见的几个知识点,这里再从源码和原理方面进行一下简析

线程池由线程池管理器,工作队列,任务队列和任务接口组成。

各个类的关系

Executor,ThreadPoolExecutor、ExecutorService、Executors这这类之间的关系我们先要梳理一下

先简单看张图吧

在这里插入图片描述

Executor是一个顶层接口,这个接口中就一个方法

public interface Executor {
    
    

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

ExecutorService就是继承Executor的接口,其中新增加了关闭线程池等方法

Executors是一个工具类,只是创建线程池的工具类而已

ThreadPoolExecutor才是线程池子类,但是我们通过Executors创建线程池返回的类为ExecutorService,这个是ThreadPoolExecutor的父类

线程池实现任务复用的原理

从executor方法进入

public void execute(Runnable command) {
    
    
    if (command == null)
        throw new NullPointerException();
    //相关源码中的注释,也描述了线程池执行任务和扩展核心线程的步骤
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
    
    
        //如果当前运行的线程小于核心线程数,则创建一个线程,这里的addWorker第二个参数为true表示创建线程时判断当前活动线程数是否大于核心线程数,如果为false,表示创建线程的时候判断运行的线程数是否大于maxPoolSize
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //检查线程池是不是运行的状态,如果是,则将任务放到等待队列中
    if (isRunning(c) && workQueue.offer(command)) {
    
    
        int recheck = ctl.get();
        //如果线程不是正在运行的状态,则删掉当前的任务并执行拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);//执行拒绝策略
        else if (workerCountOf(recheck) == 0)//如果当前没有线程执行任务,则需要创建一个线程
            addWorker(null, false);
    }
    //如果addWorker返回false,则表示是没有增加到任务队列中,则会拒绝
    else if (!addWorker(command, false))
        reject(command);
}

相关代码注释中解释了源码的工作,下面继续看一下addWorker

private boolean addWorker(Runnable firstTask, boolean core) {
    
    
    retry:
    for (int c = ctl.get();;) {
    
    
        // Check if queue empty only if necessary.
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            return false;

        for (;;) {
    
    
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateAtLeast(c, SHUTDOWN))
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
    
    
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
    
    
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
    
    
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int c = ctl.get();

                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
    
    
                    if (t.getState() != Thread.State.NEW)
                        throw new IllegalThreadStateException();
                    //根据传入的Runnable构建Worker对象,并将其加入到任务队列中
                    workers.add(w);
                    workerAdded = true;
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                }
            } finally {
    
    
                mainLock.unlock();
            }
            if (workerAdded) {
    
    
                t.start();
                workerStarted = true;
            }
        }
    } finally {
    
    
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

真正运行线程的是runWorker方法

final void runWorker(Worker w) {
    
    
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
    
    
        //不断循环调用workQueue中的线程,然后调用其run方法
        while (task != null || (task = getTask()) != null) {
    
    
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
    
    
                beforeExecute(wt, task);
                try {
    
    
                    task.run();
                    afterExecute(task, null);
                } catch (Throwable ex) {
    
    
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
    
    
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
    
    
        processWorkerExit(w, completedAbruptly);
    }
}

线程池的状态

和线程一样线程池也是有状态的,这里简单列一下线程池的几种状态

线程池的状态 含义
RUNNING 接受新任务并处理排队任务
SHUTDOWN 不接受新任务,但处理排队任务
STOP 不接受新任务,也不处理排队任务,而且还要中断正在运行的任务
TIDYING 所有任务都已经终止,workerCount为0,开始运行terminate()方法
TERMINATED 线程池完全停止

总结

简单梳理了线程池的内容,无非就是通过几个活动线程做到了任务的复用,但是使用线程池的时候,需要避免过多的创建线程,也需要避免任务过多的堆积。关于线程池的钩子函数,这里没有总结,在梳理完并发编程中锁的概念之后,再来梳理。

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/121256745