老子打烂你的 “线程池“

讲线程池之前, 先来讲讲 Callable阻塞队列

为什么有了Runnable, 还要出现 Callable?

在没有出现 Callable 之前, Java 线程都有一个缺陷 :在执行完任务之后无法获取执行结果。 如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。

而自从Java 1.5开始,就提供了 Callable 和 Future,通过它们可以在任务执行完毕之后得到任务执行结果。

Callable 与 Runnable 区别

  • Callable 需要实现的 call() 方法会抛出异常, 而 run() 方法不会抛异常
  • Callable 实现的方法 call() 有返回值, 且返回值类型就是泛型的类型, 而 run() 没有返回值

使用方式 :

FutureTask<Integer> task = new FutureTask<>(() -> 1); // 返回 1
new Thread(task).start();
// 错误实例, 线程不会使用同一个 FutureTask 对象, 主要是为了提高代码复用
new Thread(task).start();
// 获取 call() 方法的返回值
// 建议放在最后使用, 如果 call()方法没有执行完, 其他线程就会阻塞, 直到 call()方法成功返回
// 该方法还提供了一个重载方法, 可以实现超时退出
task.get()
while (!task.isDone){}//如果没有完成, 自旋

什么是阻塞队列 ?

阻塞队列在 JDK 1.5 中被引入 , 常用于生产者和消费者问题, 他为我们解决了何时阻塞线程, 何时唤醒线程的问题, 因为这一切都被 BlockingQueue 包办了 (使用通知模式实现)

  • 当队列为空时, 消费者线程就会被阻塞
  • 当队列已满时, 生产者线程就会被阻塞

BlockQueue 是一个接口, 主要有七大实现, UML 类图如下 :

  1. ArrayBlockingQueue : 数组结构组成的有界阻塞队列
  2. LinkedBlockingQueue : 链表结构组成的有界阻塞队列
  3. SynchronousQueue : 不存储元素的阻塞队列
  4. PriorityBlockingQueue : 支持优先级排序的无界阻塞队列
  5. DelayQueue : 支持延时获取元素的无界阻塞队列
  6. LinkedTransferQueue : 链表结构组成的无界阻塞队列
  7. LinkedBlockingDeque : 链表结构组成的双向阻塞队列

在这里插入图片描述
常用 API 如下 :

处理方法 会抛出异常 返回特殊值(null) 一致阻塞 超时退出
插入方法 add(E) offer(E) put(E) offer(E, long, TimeUnit)
删除 remove poll() take poll(long, TimeUnit)
检查 element() peek() 不可用 不可用

看看 offer(E, long, TimeUnit) 的源码 (poll 实现原理差不多):

public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
    checkNotNull(e);
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); 
    try {
        // 判断队列是否已满
        while (count == items.length) {
            if (nanos <= 0)// 超时推出
                return false;
            // 如果队列不可用, 调用 LockSupport#park 阻塞线程
            nanos = notFull.awaitNanos(nanos);
        }
        // 入队列
        enqueue(e);
        return true;
    } finally {
        lock.unlock();
    }
}

线程池

几乎所有需要异步并发执行任务的程序都可以使用线程池 (比如 BIO, NIO).

放在最前面 : 线程池的创建不允许使用 Executors 创建, 必须使用 new ThreadPoolExecutor 显示创建 (阿里巴巴 Java 规范)

线程池的主要特要特点: 线程复用, 控制最大并发数, 管理线程

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

Executor 框架

架构说明: Java 中的线程池是通过 Executor 框架来实现的, 该框架中使用到了 Executor, Executors, ExecutorService, ThreadPoolExecutor 这几个类, 关系如下:

在这里插入图片描述

常见的三种线程池

// 都使用ExecutorService来接收	
// 返回一个只有存有一个线程的线程池, 适用于一个任务一个任务的执行
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
----------------------------
// 返回一个可以扩容的线程池 (线程数量视具体而定), 适用于执行很多短期异步的小程序或则负载较轻的服务器
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
----------------------------
// 返回一个固定数量的线程池, 适用于执行长期任务, 性能好很多
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

可以看到, Java 自带的线程池虽然使用的是有界阻塞队列, 但是其长度使用默认值 (Integer.MAX_VALUE), 这根无界阻塞队列有何区别? 所以, 在实际开发过程中, 请使用自定义线程池

如果线程池使用无界阻塞队列会出现什么问题?

很有可能到某个时刻收到大量的任务导致线程无法及时处理, 全部往队列中塞, 由于队列是无界的, 线程池不会启用拒绝策略.

这样下去系统内存就会撑爆, 照成严重后果. 同样如果使用无界阻塞队列, 当环境出现问题的时候无法排查

线程池的几个重要参数介绍?

所有线程池调用的都是的七个参数的构造器

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          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.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

七个参数作用分别是:

  1. int corePoolSize : 线程池中常驻核心线程数

    在创建线程池之后, 当有请求任务提交, 就会安排线程池中的线程去执行请求任务
    当线程池中的线程数目达到该值时, 就会把达到的任务放在缓存队列中

  2. int maximumPoolSize : 线程池能容纳同时执行的最大线程数
  3. long keepAliveTime : 多余的空间线程的存活时间,

    当线程池数量超过 corePoolSize , 当空闲时间达到该值时, 多余空闲线程会被销毁直到只剩下 corePoolSize 线程数为止( 比方说, 这个活两个人干就够了, 还有三个人在吃瓜, 老板就让他们三个直接滚蛋, keepAliveTime 就相当于吃瓜时间)

  4. TimeUnit unit : keepAliveTime 的单位
  5. BlockingQueue workQueue : 任务队列, 被提交但未被执行的任务

    如果队列已满, 但仍然收到被提交的请求, 就会开启新的线程来处理任务队列中的任务, 直到线程数达到maximumPoolSize ( 比方说, 银行只有两个工作窗口, 候客区也坐满了, 但此时仍有人来办理业务, 大堂经理就会打电话通知其他人来上班)

  6. hreadFactory threadFactory : 表示生成线程池中工作线程的线程工厂, 用于线程 (一般使用默认的即可)

    可以实现 ThreadFactory 接口, 自定义线程组名称,在 jstack 问题排查时,非常有帮助

  7. RejectedExecutionHandler handler : 拒绝策略

    表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize )时如何拒绝提交的请求, 不让这些请求放入缓存队列中

如何设置最佳的 maximumPoolSize?

需要考虑两点 : 1. 尽量减少线程切换和管理的开支; 2. 最大化利用cpu

高并发, 任务执行时间短

建议少线程,只要满足并发即可 , 由于执行时间短, 就要减少线程的切换和管理开销, 比如并发数为 200, 将 maximumPoolSize 设置为 20 即可

并发不高, 任务执行时间长

建议多线程,保证有空闲线程,接受新的任务;由于任务执行时间长, 需要额外的线程执行其他任务

并发高, 任务执行时间长

由任务的 CPU 密集型, 和 I/O 密集型 来决定, 可以通过 Runtime.getRuntime().availableProcessors() 来获取此计算机的CPU核心数

  • CPU 密集意思是该任务需要大量的运算, 而没有阻塞, CPU 一直全速运行, CPU 密集任务只有在真正的多核 CPU 才有可能得到加速 (通过多线程), CPU 密集型任务配置尽可能少的线程数量. 一般公式: maximumPoolSize = CPU核心数+1
  • IO 密集型任务分两种情况
    第一种 IO 密集型任务, 并不是一直在执行任务,所以通常就需要开CPU核心数两倍的线程, maximumPoolSize = CPU核心数 * 2,
    第二种 IO 密集型任务需要大量的 IO, 即大部分线程被阻塞, 需要多配置线程数. 参考公式: maximumPoolSize = CPU核心数 / ( 1 - 阻塞系数), 阻塞系数一般在 0.8~0.9 之间, 8 核 CPU : 取阻塞系数乐观值 0.9 , maximumPoolSize = 80

线程池的工作流程 (工作原理)?

  • 当线程池创建好之后, 等待提交过来的的任务请求

    可以使用两种方法向线程池提交任务, 分别是 execute() 和 submit()
    前者用于提交不需要返回值的任务, 任务是一个 Runnable 实例
    后者用于提交需要返回指定任务, 任务是一个 Callable 实例, 该方法会返回一个 Future 对象

  • 当调用 executor() 方法提交一个任务时, 线程池会做以下判断:

    • 如果正在运行的线程数小于 corePoolSize, 那么立刻创建线程执行这个任务
    • 如果正在运行的线程数大于或等于 corePoolSize, 那么将会把任务放入缓存队列中
    • 如果这个时候队列满了且正在运行的线程数小于 maximumPoolSize , 这继续创建非核心线程运行这个任务
    • 如果队列满了且正在运行的线程等于 maximumPoolSize , 那么线程池就会启用拒绝策略
  • 当一个线程执行完一个任务后, 他就会从队列中去取下一个任务继续执行

  • 当一个线程无事可做时超过设定的时间时, 那么线程就会做出判断: 如果当前运行的线程数大于 corePoolSize, 那么这个线程就会被停掉, 直到正在运行的线程数减少到 corePoolSize 大小

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    //  运行线程数小于 corePoolSize 
    if (workerCountOf(c) < corePoolSize) {
        // 创建线程放入线程池中,并且执行当前任务
        if (addWorker(command, true))
            return;
        // 创建线程失败
        c = ctl.get();
    }
    //  将任务放入放入任务队列中 (队列未满)
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 判断工作线程是否大于 maximumPoolSize, 如果是,则把任务移除队列
        if (! isRunning(recheck) && remove(command))
            // 通过拒绝策略对该任务进行处理
            reject(command);
        // 否则, 创建线程执行当前任务
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 队列已满, 创建线程放入线程池中,并且执行当前任务
    else if (!addWorker(command, false))
        // 队列已满且工作线程是否大于 maximumPoolSize
        // 通过拒绝策略对该任务进行处理
        reject(command);
}

线程池的关闭方式有几种,各自的区别是什么?

可以通过调用线程池的 shutdownshutdownNow 方法来关闭线程池 , 他们的原理是遍历线程池中的工作线程, 然后逐个调用其 interrupt 方法来中断线程, 所以无法响应中断的任务可能永远无法终止.

  • shutdownNow 首先将线程池的状态设置为 STOP, 然后尝试停止所有的正在执行或暂停任务的线程, 并返回等待执行任务的列表

  • shutdown 只是将线程池的状态设置为 SHUTDOWN 状态, 然后中断所有没有正在执行任务的线程

只要调用了这两个方法中的任意一个, isShutdown 方法就会返回 true , 当所有的任务都已关闭后, 才表示线程池关闭成功, 这是调用 isTerminaed 方法会返回 true , 只与应该调用哪一种方法来关闭线程池, 应该由提交到线程池的任务的特性决定, 通常调用 shutdown 方法关闭线程池, 如果任务不一定要执行完, 也可以调用 shutdownNow 方法

单机上一个线程池正在处理服务如果突然断电怎么办? 正在处理和阻塞队列里的任务如何处理?

可以对正在处理的任务和阻塞队列的任务做 事物管理 或者对阻塞队列中的任务 持久化处理

当断电或者系统崩溃,操作无法继续下去的时候,可以通过 回溯日志 的方式来撤销正在处理的已经执行成功的操作。然后重新执行整个阻塞队列。

拒绝策略

public interface RejectedExecutionHandler{...} // 这是一个接口, 一共有四个实现类

  1. AbortPolicy: 默认使用这个策略, 直接抛出 RejectExecutorException 异常阻止系统正常运行
  2. CallerRunsPolicy : “调用者运行” 一种调节机制, 该策略即不会抛弃任务, 也不会抛异常, 而是将某些任务回退到调用者, 从而降低新的任务流量
  3. DiscardOldestPolicy : 抛弃队列中等待最久的任务, 然后把任务加入队列尝试再次提交任务
  4. DiscardPolicy : 直接丢弃任务, 不予任何处理也不抛异常. 如果允许任务丢失, 这是最好的一种方案

猜你喜欢

转载自blog.csdn.net/Gp_2512212842/article/details/107316534