Java线程池详解

Java线程池解析

在Java中有两种方式创建线程池,一种是直接使用Executors工具类创建预先定义好的线程池。一共有以下四种线程池

  • newCachedThreadPool:可缓存的无边界的线程池,最大线程数Integer.MAX_VALUE

    public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>());
    }
  • newFixedThreadPool:定长的线程池,超出则等待,线程不可扩容

    public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());
    }
  • newScheduledThreadPool:定长的线程池,支持定时及周期性任务的执行。最大线程数Integer.MAX_VALUE。

    public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
    DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
    new DelayedWorkQueue());
    }
  • newSingleThreadExecutor:单线程的线程池,保证所有任务FIFO执行。

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

可以看到这四个线程池中涉及到了三个阻塞队列,根据阻塞队列的不同,线程池也提供了不一样的功能

  • SynchronousQueue:这是一个无缓冲的队列,意思是不会存储元素,每个插入操作必须等待移除操作完成,它没有容量的概念,因此被称作“传递性”队列。当线程池中不允许任务等待,需要立刻创建新线程执行时使用。newCachedThreadPool使用了这个队列,所以在newCachedThreadPool中,如果线程没有空闲的,会立刻创建新线程来执行新任务。

  • LinkedBlockingQueue:基于链表的阻塞队列,按照FIFO的原则对元素进行排序,内部使用锁机制来保证多线程的安全性,可以设置容量大小。

  • DelayedWorkQueue:支持延时获取元素的阻塞队列,是一个基于堆的阻塞队列,插入删除的效率都是logN,可以理解为优先级队列,可以为每个元素设置到期时间,只有延迟时间到了,才能取走。适用于那些需要延迟处理的任务,如缓存失效、任务调度等。

讲完了内置的预定义的四个线程池,但是一般开发中不会使用这四个线程池,因为内置的不可变参数可能导致程序产生问题,例如线程数无限增加,一般来说我们希望定义一个任何操作都是可控的线程池,所以一般使用ThreadPoolExecutor进行线程池的创建。

使用ThreadPoolExecutor创建线程池

使用ThreadPoolExecutor创建线程池需要指定7个参数

  • 核心线程数corePoolSize

  • 最大线程数maximumPoolSize

  • 线程生存时间keepAliveTime

  • 线程生存时间的单位TimeUnit

  • 阻塞队列BlockingQueue

  • 线程工厂ThreadFactory

  • 拒绝策略RejectedExecutionHandler

前面四个参数顾名思义,这里主要讲一下后面三个参数

阻塞队列

Java常用的阻塞队列一般有八个

ArrayBlockingQueue:基于数组的阻塞队列,需要初始化大小,因此是有界的。

LinkedBlockingQueue:基于链表的阻塞队列,最大长度MAX_VALUE,因此可以理解为无解的,可以指定大小。(常用)内部使用锁保证多线程安全性。

PriorityBlockingQueue:优先级队列,可以自定义比较器

DelayQueue:延迟队列,也可以理解为优先级队列

DelayedWorkQueue:实现和DelayQueue一样,不过是将优先级队列和DelayQueue的实现过程迁移到本身方法体中,从而可以在该过程当中灵活的加入定时任务特有的方法调用。可以理解为DelayQueue增强版。

SynchronouseQueue:上面说的“传递性”队列

LinkedTransferQueue:实现了一个重要功能:如果有等待的空闲线程,put操作直接将任务传递给线程,不会入队,一定程度上实现了插队的功能。可以理解为 LinkedBolckingQueue 和 SynchronousQueue 和合体。

LinkedBlockingDeque:顾名思义,是一个双向链表的队列,支持FIFIO和FILO操作,可以在队头和队尾同时操作。

线程工厂

线程工程是一个接口,内容很简单:收一个Runnable对象,并将其封装到Thread对象中,进行执行。

public interface ThreadFactory {
/**
* Constructs a new {@code Thread}. Implementations may also initialize
* priority, name, daemon status, {@code ThreadGroup}, etc.
*
* @param r a runnable to be executed by new thread instance
* @return constructed thread, or {@code null} if the request to
* create a thread is rejected
*/
Thread newThread(Runnable r);
}

因此我们可以自定义一个线程工厂,实现这个接口就行

public class MyFactory implements ThreadFactory{
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
}

实际使用过程中可以添加自定义功能:例如线程名称、线程计数、线程总数控制等。

拒绝策略

拒绝策略是指,如果最大线程数到了并且等待队列满了之后,新到的任务应该怎么处理,JUC中定义了四种拒绝策略,如下:

  • AbortPolicy:拒绝并抛出异常

  • CallerRunsPolicy:拒绝并且调用者运行策略,谁给我的谁运行

  • DiscardOldestPolicy:抛弃最早未处理任务

  • DiscardPolicy:直接丢弃任务,啥也不管

讲完了线程池的概述,我们来讲一下线程池的运行逻辑

运行逻辑

运行逻辑是指一个新的任务提交,应该怎么做?

  • 是否达到核心线程数?

    • 没有达到则创建一个新的线程执行任务

    • 注意:如果没有达到核心线程数,但是有空闲线程,是创建新的还是复用旧的?答案是创建新的线程。

  • 是否队列已满?

    • 没有满就加入工作队列中
  • 是否达到最大线程数?

    • 没有达到最大线程数,就会创建新线程执行任务

    • 达到了就进入拒绝策略

也就是说,判断顺序是:是否大于核心线程数---队列是否已满---是否大于最大线程数

关于线程池的一些问题?

  • 如果队列中的任务失效了怎么处理?

    • 使用Future.cancel是可以取消一个任务的,原理是向正在运行任务的线程发送中断指令,即Interrupt方法。

    • 如果任务没有开始执行,会仍然存在队列中,直到被拉取,拉取时发现Interrupt后不会执行。

    • 如果任务正在运行切支持响应中断,会抛出InterruptedException并提前中止。

  • 如果线程执行过程中异常停止了怎么办?线程池怎么处理异常

    首先提交任务可以使用execute方法或者submit方法

    • execute方法不会抛出异常给调用者,异常会被吞掉,但是可以重新uncaughtException方法进行异常主动处理。并且线程池内部会打印异常信息,这里指的被吞掉是指调用者无法感知。

    • submit方法会返回一个Future对象,抛出的异常会被封装在内,使用get获取返回值的时候会抛出异常。出现异常之后线程池也不会打印异常信息。

其次一个线程出现异常后,这个线程会被销毁,创建一个新的干净的线程放到线程池中。销毁线程是为了防止旧的线程中有脏数据影响新任务执行。

  • 如何优雅的关闭一个线程池

    首先调用shutdown方法发送关闭指令,启动一个有序的关闭过程,已经提交的任务会继续执行,但是不会接收新的任务。

    其次等待一定的可接受的时间threadPool.awaitTermination(为了给正在运行的任务时间),超时之后调用shutdownNow方法,强行退出,退出原理是给线程打上中断,但是中断不意味着能终止。

    然后再等待一定的时间,还没有结束就做失败处理。。。

  • 线程池是怎么知道线程完成了任务空闲了

    这个问题其实可以转变一下思路,改成

    线程是怎么实现复用的?空闲就是线程复用呗

    线程在线程池内部其实封装成了一个worker对象

    Worker extends AbstractQueuedSynchronizer

    可以看到继承了AQS,所以Worker自身是具有锁的特性的。在创建 Worker 对象的时候,会把线程和任务一起封装到 Worker 内部,然后调用 runWorker 方法来让线程执行任务

    实现线程复用的逻辑就在runWorker方法中,由于使用了while循环,当第一个任务执行完成后,会不断通过getTask获取任务,只要能获取到任务,就会调用run方法执行任务。

    并且getTask内部也是一个死循环,除非被一些原因退出(生存时间到、线程池关闭等)

    如果获取不到getTask退出,就会执行finally中的processWorkerExit方法,将线程退出。

    因为 Worker 继承了 AQS,每次在执行任务之前都会调用 Worker 的 lock 方法,执行完任务之后,会调用 unlock 方法,这样做的目的就可以通过 Woker 的加锁状态判断出当前线程是否正在执行任务。

    如果想知道线程是否空闲,只需要调用Worker的tryLock方法,加锁成功就是空闲。

    final void runWorker(Worker w) {
    // 获取当前工作线程
    Thread wt = Thread.currentThread();
    // 从 Worker 中取出第一个任务
    Runnable task = w.firstTask;
    w.firstTask = null;
    // 解锁 Worker(允许中断)
    w.unlock();
    boolean completedAbruptly = true;
    try {
    // 当有任务需要执行或者能够从任务队列中获取到任务时,工作线程就会持续运行
    while (task != null || (task = getTask()) != null) {
    // 锁定 Worker,确保在执行任务期间不会被其他线程干扰
    w.lock();
    // 如果线程池正在停止,并确保线程已经中断
    // 如果线程没有中断并且线程池已经达到停止状态,中断线程
    if ((runStateAtLeast(ctl.get(), STOP) ||
    (Thread.interrupted() &&
    runStateAtLeast(ctl.get(), STOP))) &&
    !wt.isInterrupted())
    wt.interrupt();
    try {
    // 在执行任务之前,可以插入一些自定义的操作
    beforeExecute(wt, task);
    Throwable thrown = null;
    try {
    // 实际执行任务
    task.run();
    } catch (RuntimeException x) {
    thrown = x; throw x;
    } catch (Error x) {
    thrown = x; throw x;
    } catch (Throwable x) {
    thrown = x; throw new Error(x);
    } finally {
    // 执行任务后,可以插入一些自定义的操作
    afterExecute(task, thrown);
    }
    } finally {
    // 清空任务,并更新完成任务的计数
    task = null;
    w.completedTasks++;
    // 解锁 Worker
    w.unlock();
    }
    }
    completedAbruptly = false;
    } finally {
    // 工作线程退出的后续处理
    processWorkerExit(w, completedAbruptly);
    }
    }
  • 如何设置线程池的核心线程数和最大线程数?

  • 线程池怎么优化?

  • 如何自己实现一个线程池?

  • 什么是线程池?为什么使用线程池?

  • 什么是线程池的预热机制?

  • 线程池的优点和缺点是什么?

  • 线程池中的线程是怎么执行任务的?

  • 线程池中的任务可以返回执行结果吗?

  • 线程池的内存泄漏问题?

  • 在多线程的环境下,怎么保证线程池中的任务按照特定的顺序执行?

  • 日常工作中有用到线程池吗?什么是线程池?为什么要使用线程池?

  • ThreadPoolExecutor使用到了哪些锁?为什么要使用锁?

  • 线程池运行过程中怎么监控?

  • execute提交任务和submit提交任务有什么不同?

posted @   枫叶藏在眼眸  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端
历史上的今天:
2020-08-24 【Java数据结构】循环队列的数组实现
2020-08-24 【Java数据结构】普通矩阵与稀疏矩阵的互相转化,稀疏矩阵的物理存储
点击右上角即可分享
微信分享提示