java线程池

线程池

1、为什么要用线程池(线程池的优点)

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

2、线程池的内部原理

知道了为什么要用线程池之后,那么我们看看线程池到底是怎么样的?
我们结合ThreadPoolExecutor 类分析
ThreadPoolExecutor 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)

/**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//设置keepAliveTime的时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。
ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

阻塞队列 BlockingQueue workQueue
在任意时刻,不管并发有多高,永远只有一个线程能够进行队列的入队或者出队操作。线程安全的队列, 队列满了,只能进行出队操作,所有入队的操作必须等待,也就是被阻塞 相反的 队列空了,只能进行入队操作,所有出队的操作必须等待,也就是被阻塞

  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  • unit : keepAliveTime 参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :饱和策略。关于饱和策略下面单独介绍一下。

2.1、线程池的内部原理
先上一张图
在这里插入图片描述
在这里插入图片描述
线程是用来处理任务的,现在有一个线程池(核心线程(corePoolSize)是2,最大线程(maximumPoolSize)是3,任务队列(workQueue)是5),空闲一个线程
当提交一个任务(这个任务需要实现Runnable/Callable接口),先去线程池中判断线程池中的核心线程数(corePoolSize)是否已经满了,如果没有满的话,创建一个核心线程(corePoolSize)去处理这个任务;如果核心线程数(corePoolSize)满了的话,判断队列(BlockingQueue<Runnable> workQueue)是否已经满了,如果没有满的话,就加入这个队列,如果满了的话,会创建这个空闲的线程去处理这个任务。
但是如果这个线程池和队列都满了,这个时候线程池提供了一个拒接策略RejectedExecutionHandler handler
在这里插入图片描述
线程池由两个核心数据结构组成:

  • 1)线程集合(workers):线程池创建线程时,会将线程封装成工作线程Worker,存放执行任务的线程,是一个HashSet;
  • 2)任务等待队列(workQueue):存放等待线程池调度执行的任务,是一个阻塞式队列BlockingQueue;

2.2、提交方式execute() vs submit()

在提交任务的时候有两种方式,上图中提交的方式是execute提交,这两种方式的区别如下

  • 1、execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  • 2、submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
    我们以AbstractExecutorService接口中的一个 submit 方法为例子来看看源代码:
 public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

上面方法调用的 newTaskFor 方法返回了一个 FutureTask 对象。

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

我们再来看看execute()方法:

public void execute(Runnable command) {
      ...
    }

2.3、这个任务等待队列(BlockingQueue workQueue)

阻塞队列 BlockingQueue workQueue
在任意时刻,不管并发有多高,永远只有一个线程能够进行队列的入队或者出队操作。线程安全的队列
队列满了,只能进行出队操作,所有入队的操作必须等待,也就是被阻塞 相反的
队列空了,只能进行入队操作,所有出队的操作必须等待,也就是被阻塞

workQueue是一个BlockingQueue接口的实现类,存放Runnable对象
在这里插入图片描述
一般来说,该队列的选择常用如下四种:

  • (1)ArrayBlockingQueue:【有界的任务队列,按照先进先出的顺序出队】,该队列的构造方法为public ArrayBlockingQueue(int capacity),初始化时必须设置其容量。当线程池中的实际线程数小于corePoolSize,则优先创建新的线程;如果大于corePoolSize,并且此时恰好没有空闲线程,则优先进入有界队列,队列满了以后,再从队列中出队任务创建线程,直到线程池中的线程数量maximumPoolSize。当大maximumPoolSize后,执行拒绝策略。
  • (2)SynchronousQueue:【直接提交的队列】,容量为0,每次插入都必须等待一个任务的删除操作每次删除也要等待一个任务的插入操作。使用该队列,由于容量为0,其不会真实的保存任务,而是每次都将新任务提交给线程池,如果线程池满了,则直接执行拒绝策略,简单粗暴。
  • (3)LinkedBlockingQueue:【无界的任务队列,按照先进先出的顺序出队】,当线程池中的实际线程数小于corePoolSize,则优先创建新的线程;如果大于corePoolSize,并且此时恰好没有空闲线程,只要服务器资源足够多,就会无限制的进入入队操作,直到资源耗尽。所以使用了该任务队列的线程池最大支持线程数为corePoolSize,所以使用该队列的创建线程池的方法一般是corePoolSize和maximumPoolSize相等的newFixedThreadPool。默认最大长度为Integer.MAX_VALUE,这在用做线程池队列的时候,会比较危险。
  • (4)PriorityBlockingQueue:【优先任务队列,按照任务的优先级出队】,带有任务执行优先级的队列,可以控制任务执行的先后顺序,在确保系统性能的同时,对质量也有了保证。
  • (5)LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue和SynchronousQueue的结合体,但是把它用在ThreadPoolExecutor中,和LinkedBlockingQueue行为一致,但是是无界的阻塞队列

注意有界队列和无界队列的区别:
如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;
而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置maximumPoolSize没有任何意义。

2.4、ThreadPoolExecutor 饱和策略定义(拒绝策略):

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:默认的拒绝策略,直接抛出异常 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
    举个例子:

Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。)

3、案例一:Callable+ThreadPoolExecutor示例代码

了解了上面的原理之后,我们先来一个案例巩固一下

MyCallable.java

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        //返回执行当前 Callable 的线程名字
        return Thread.currentThread().getName();
    }
}

CallableDemo.java

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CallableDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {

        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        List<Future<String>> futureList = new ArrayList<>();
        Callable<String> callable = new MyCallable();
        for (int i = 0; i < 10; i++) {
            //提交任务到线程池
            Future<String> future = executor.submit(callable);
            //将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值
            futureList.add(future);
        }
        for (Future<String> fut : futureList) {
            try {
                System.out.println(new Date() + "::" + fut.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        //关闭线程池
        executor.shutdown();
    }
}

输出结果:

Wed Nov 13 13:40:41 CST 2019::pool-1-thread-1
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-2
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-4
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-5
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-2
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-1
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-4
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5

4、案例二:Runnable+ThreadPoolExecutor

首先创建一个 Runnable 接口的实现类(当然也可以是 Callable 接口,上面的案例一)

MyRunnable.java

import java.util.Date;

/**
 * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
 * @author shuang.kou
 */
public class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String s) {
        this.command = s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}

ThreadPoolExecutorDemo.java

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

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {

        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
            Runnable worker = new MyRunnable("" + i);
            //执行Runnable
            executor.execute(worker);
        }
        //终止线程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

可以看到我们上面的代码指定了:

  • 1、corePoolSize: 核心线程数为 5。
  • 2、maximumPoolSize :最大线程数 10
  • 3、keepAliveTime : 等待时间为 1L。
  • 4、unit: 等待时间的单位为 TimeUnit.SECONDS。
  • 5、workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100;
  • 6、handler:饱和策略为 CallerRunsPolicy。

Output:

pool-1-thread-3 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-5 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-2 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-1 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-4 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-3 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-4 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-1 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-5 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-1 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-2 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-5 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-4 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-3 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-2 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-1 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-4 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-5 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-3 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020

5、线程池的5种状态

线程池有5种状态,分别是:

  • 1、Running: 能接受新任务以及处理已添加的任务;
  • 2、ShutDown: 不接受新任务,可以处理已经添加的任务;但是队列里的任务得执行完毕
  • 3、Stop: 指调用了 shutdownNow() 方法,不接受新任务,不处理已经添加的任务,并且中断正在处理的任务;
  • 4、Tidying:所有的任务已经终止,ctl记录的“任务数量”为0,ctl负责记录线程池的运行状态与活动线程数量;
  • 5、Terminated: 线程池彻底终止,则线程池转变为Terminated状态;
    在这里插入图片描述
    shutdown() VS shutdownNow()
  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
  • shutdownNow() :关闭线程池,线程的状态变为STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。

isTerminated() VS isShutdown()

  • isShutDown 当调用 shutdown() 方法后返回为 true。
  • isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

6、线程池的使用场景

线程池适合单系统的大量的异步任务处理,比如发送短信、保存日志。

7、创建线程池

7.1、创建线程池的话一般会创建这几种线程池:

  • (1)newSingleThreadExecutor:这是一个单线程池,至始至终都由一个线程来执行。
  • (2)FixedThreadPool:可重用固定线程数的线程池
  • (3)ScheduledThreadPool:定时线程池,用来在给定的延迟后运行任务,或者定期执行任务——这个可以跟定时任务结合
  • (4)CachedThreadPool:可缓存线程池,会根据需要创建新线程的线程池
    在这里插入图片描述

Executors 返回线程池对象的弊端如下:
FixedThreadPool和SingleThreadExecutor:主要问题是堆积的请求处理队列均采用LinkedBlockingQueue,允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,会耗费非常大的内存,甚至OOM。
CachedThreadPool和ScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

7.2、上面四种线程池的创建方式

  • 方式一:通过 Executor 框架的工具类 Executors 来实现
  • 方式二:通过ThreadPoolExecutor构造函数实现(推荐)

《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写更加明确线程池的运行规则,规避资源耗尽的风险

说白了就是:使用有界队列,控制线程创建数量

除了避免 OOM 的原因之外,不推荐使用 Executors 提供的两种快捷的线程池的原因还有:

  • (1)实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
  • (2)我们应该显示地给我们的线程池命名,这样有助于我们定位问题。

这里的具体创建过程和原理说明可以参考:
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/java%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md

8、监测线程池运行状态

8.1、监控线程池的运行状态都监控的是什么?怎么监控的?
你可以通过一些手段来检测线程池的运行状态比如 SpringBoot 中的 Actuator 组件。

除此之外,我们还可以利用 ThreadPoolExecutor 的相关 API做一个简陋的监控。
从下图可以看出, ThreadPoolExecutor提供了获取线程池当前的线程数活跃线程数已经执行完成的任务数正在排队中的任务数等等。

8.2、检测线程池的案例
printThreadPoolStatus()会每隔一秒打印出线程池的线程数、活跃线程数、完成的任务数、以及队列中的任务数。

/**
     * 打印线程池的状态
     *
     * @param threadPool 线程池对象
     */
    public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
        ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-thread-pool-status", false));
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            log.info("=========================");
            log.info("ThreadPool Size: [{}]", threadPool.getPoolSize());
            log.info("Active Threads: {}", threadPool.getActiveCount());
            log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount());
            log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
            log.info("=========================");
        }, 0, 1, TimeUnit.SECONDS);
    }

8.3、建议不同类别的业务用不同的线程池
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/best-practice-of-threadpool.md

8.4、给线程池命名
初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题。
默认情况下创建的线程名字类似 pool-1-thread-n 这样的,没有业务含义,不利于我们定位问题。

给线程池里的线程命名通常有下面两种方式:

  • (1)利用 guava 的 ThreadFactoryBuilder
ThreadFactory threadFactory = new ThreadFactoryBuilder()
                        .setNameFormat(threadNamePrefix + "-%d")
                        .setDaemon(true).build();
ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory)
  • (2)自己实现 (implements)ThreadFactor。
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * 线程工厂,它设置线程名称,有利于我们定位问题。
 */
public final class NamingThreadFactory implements ThreadFactory {

    private final AtomicInteger threadNum = new AtomicInteger();
    private final ThreadFactory delegate;
    private final String name;

    /**
     * 创建一个带名字的线程池生产工厂
     */
    public NamingThreadFactory(ThreadFactory delegate, String name) {
        this.delegate = delegate;
        this.name = name; // TODO consider uniquifying this
    }

    @Override 
    public Thread newThread(Runnable r) {
        Thread t = delegate.newThread(r);
        t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
        return t;
    }

}

9 、线程池大小的确定

既然知道的线程池的原理以及怎么创建线程池,那在工作中创建线程池的时候怎么确定线程池的核心线程、最大线程数,队列大小等等呢?

我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。
如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。
但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

上下文切换:
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

有一个简单并且适用面比较广的公式:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
  • 混合型任务

N+1 为什么不是N+2或者N+3呢 还有2N ——这是经验值
CPU密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。
同样以第一种方式来看,对于IO密集型应用,假定所有的操作时间几乎都是IO操作耗时,那么W/C的值就为1,那么对应的线程数确实为2N。

如果是cpu密集型的,尽量减少线程数,如果是IO密集型任务尽量加大线程数,因为io不占用cpu的资源。建议配置2倍CPU个数+1。
如果是混合型的,尽量根据实际情况进行拆分,根据运行时间来决定。

如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

  • 线程数的设置需要考虑三方面的因素
    服务器的配置、服务器资源的预算和任务自身的特性。
    具体来说就是服务器有多少个CPU,多少内存,IO支持的最大QPS是多少,任务主要执行的是计算、IO还是一些混合操作,任务中是否包含数据库连接等的稀缺资源。线程池的线程数设置主要取决于这些因素。

一问:那假如在一个请求中,计算操作需要5ms,DB操作需要100ms,对于一台8个CPU的服务器,怎么设置线程数呢?
答:这是一个计算和IO混合型的任务,可以将其分解为两个线程池来处理。一个线程池处理计算操作,设置N+1=9个线程,一个线程处理IO操作,设置2N=16个线程。
二问:如果一个任务同时包含了一个计算操作和DB操作呢,不能拆分怎么设置?你能讲一下具体的计算过程吗?
答:首先这个任务整体上是一个IO密集型的任务。在处理一个请求的过程中,总共耗时100+5=105ms,而其中只有5ms是用于计算操作的,CPU利用率为5/(100+5)。使用线程池是为了尽量提高CPU的利用率,减少对CPU资源的浪费,假设以100%的CPU利用率来说,要达到100%的CPU利用率,对于一个CPU就要设置其利用率的倒数个数的线程数,也即1/(5/(100+5))计算密集任务所占比重,8个CPU的话就乘以8。那么算下来的话,就是……168,对,这个线程池要设置168个线程数。(最佳线程数=CPU核数*(1+(IO耗时/CPU耗时))
三问:如果实际的任务差异较大,不同任务实际的CPU操作耗时和IO操作耗时有所不同,那么怎么设置线程数呢?
:那对所有任务的CPU操作耗时和IO操作耗时求个平均值就好了。
四问:那如果现在这个IO操作是DB操作,而DB的QPS上限是1000,这个线程池又该设置为多大呢?
:按比例来减少就可以了,按照之前的计算过程,可以计算出来当线程数设置为168的时候,DB操作的QPS为,168(1000/(100+5))=1600,如果现在DB的QPS最大为1000,那么对应的,最大只能设置168(1000/1600)=105个线程。

除了考虑任务CPU操作耗时、IO操作耗时之外,还需要服务器的内存资源、硬盘资源、网络带宽等等的。

线程数的第一种计算方法:

在一个基准负载下,使用 几种不同大小的线程池运行你的应用程序,并观察CPU利用率的水平。
给定下列定义:
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
W/C = 等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的池的大小等于:Nthreads = Ncpu x Ucpu x (1 + W/C)
W/C也是可以通过基准程序测试得出的

线程数的第二种计算方法

线程数 = CPU可用核心数/(1 - 阻塞系数),其中阻塞系数的取值在0和1之间。
计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。一个完全阻塞的任务是注定要挂掉的,所以我们无须担心阻塞系数会达到1。阻塞系数可以通过公式:阻塞系数=阻塞时间/(阻塞时间+计算时间)

SpringBoot 使用线程池

既然用了 SpringBoot ,那自然得发挥 Spring 的特性,所以需要 Spring 来帮我们管理线程池:

参考:
Java线程池实现原理及其在美团业务中的实践

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值