IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 优雅地理解线程池源码 -> 正文阅读

[Java知识库]优雅地理解线程池源码

线程池源码

网上的一些博客对线程池的讲解都是逐行解读源码,看起来可能会比较费力,本文从功能角度出发,以整个流程为切入点,省去一些没必要的源码,带你逐层抽丝剥茧,理解线程池设计的精髓所在。

前置知识

几个常量和变量

// ctl是一个int类型的组合变量(32个bit),低29位表示线程池里的线程数,高3位表示线程池的状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// 所有在线程池中的线程都会被放到HashSet进行保存
private final HashSet<Worker> workers = new HashSet<Worker>();

// 阻塞队列
private final BlockingQueue<Runnable> workQueue;

内部类

Worker简而言之就是对工作线程进一步的封装,不管你是核心线程还是非核心

private final class Worker{
  // 指向当前线程
  final Thread thread;
  // 第一个任务,这里提示线程池是懒初始化,后面具体再谈
  Runnable firstTask;
}

线程池的执行过程

通过execute函数我们宏观把握一下整个流程,然后抽丝剥茧逐层递进。这里需要单独说一下addWorker函数,他是用于创建新线程的,他的第二个参数传入一个boolean值,用于区分是核心线程和非核心线程的,后面再深入研究。

下面就是整个流程,没框起来的不用关注
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D97qLxoX-1651998722488)(/Users/mac/Library/Application%20Support/typora-user-images/image-20220508100145720.png)]
这里还需要提一嘴的是,addWorker就是创建新线程,也就是说我们的线程池是懒初始化的,一开始线程池并没有任何线程,直到有需求过来了才开始逐步创建线程

线程如何在线程池中流转

上面提到,当外界调用线程池的excute函数执行我们的任务时,会通过addWorker函数新建一个线程去执行,那么该线程执行完这个任务之后又何去何从呢?下面进行分析。

其实当我们调用addWorker之后,会执行runWorker函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t7j8DvwE-1651998722491)(/Users/mac/Library/Application%20Support/typora-user-images/image-20220508105308909.png)]
因此,我们可以知道,我们的线程在线程池中的一个流转过程大概是这样的:首先线程池的设计是一个懒初始化,当我们提交任务的时候执行任务的线程才会被创建出来,当他执行完这第一个任务之后,他就不断地使用getTask函数尝试去阻塞队列里面拿任务进行执行,如果阻塞队列里面没有任务了,那么这个线程会被阻塞挂起,直到阻塞队列有任务了,会唤醒该线程继续去拿任务执行。而线程的退出我会在下文进行讲解。

非核心线程在空闲一段时间后如何回收

我们知道,当阻塞队列满了,而线程数又没有超过最大线程数的时候,新来的任务会开启新的线程来执行,这些线程称之为非核心线程。而当阻塞队列为空,也就是没有新的任务可以执行,而非核心线程闲置一段时间后,是要被回收的,那么这个回收是如何进行的呢?下面对getTask函数进行详细解读,这里是线程池的一个精髓所在

跟着图中的1,2,3,4的顺序看,这样看起来更容易理解
在这里插入图片描述
那么总结一下,关键点在于,getTask是如何从队列中取任务的,当线程数小于等于核心线程数,使用的是阻塞队列的take方法,有任务就取任务,没任务就阻塞。当线程数大于核心线程数,表明有非核心线程参与工作,使用的是阻塞队列的poll方法,传入一个时间参数,有任务直接取任务,没任务的时候不会像take那样一直阻塞等待,而是等keepAliveTime时间后如果还等不到就返回null。而我们知道,如果说getTask返回null,runWorker就不会一直while循环,而是直接退出了,这也就是非核心线程被回收的原理!

如何优雅地关闭线程池——shutdown和shutdownNow

了解关闭线程池之前先了解一下如何关闭一个线程

如何优雅地关闭一个线程

java提供了stop和destroy函数可以强制杀死线程,但是官方是不建议使用了,为什么呢?因为强制杀死线程的话,可能会导致一些资源,比如文件描述符、网络连接等没有正常关闭,浪费系统资源。所以优雅地关闭线程应该是,让线程执行完后自动关闭。

大概可以有两种方式来实现:

1.设置中断标志位

class MyThread extends Thread{
  private boolean stopped = false;
  
  @Override
  public void run(){
    while(!stopped){
      ...
    }
  }
}

我们可以通过外界线程来设置stopped标志来让该线程退出,从而结束。那么这样做有一个坏处,当while循环中有阻塞方法,比如wait,那么该线程可能会永远关闭不了。因此,引入第二种方法interrupt

2.interrupt函数

class MyThread extends Thread{
  private boolean stopped = false;
  
  @Override
  public void run(){
    while(!Thread.currentThread().isInterrupted()){
      ...
    }
  }
}

然后外界调用该线程interrupt方法就可以结束该程序,与第一个方法不同的是,他可以响应InterruptedException,下面几个函数就是这样:

public static native void sleep() throws InterruptedException;
public final void wait() throws InterruptedException;
public final void join() throws InterruptedException;

线程池状态变化

官方给出的五种线程池状态如下:

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;

RUNNING标记为-1,其余都是为正数,从左到右越来越大,不可逆。其中主要是shutdown和shutdownNow两个的区别,这里着重分析一下:

看下面的对比,其实就两处不同,先设置不同的线程状态,然后分别执行各自的中断函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3oUeBjfH-1651998722494)(/Users/mac/Library/Application%20Support/typora-user-images/image-20220508160929757.png)]

因此,这里着重分析一下interruptIdleWorkers()和interruptWorkers()

interruptIdleWorkers()

shutdown只是关闭掉那些空闲的线程,阻塞队列里面的任务还是要执行的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pvgB7G7A-1651998722494)(/Users/mac/Library/Application%20Support/typora-user-images/image-20220508161754879.png)]

interruptWorkers()

shutdownNow()是直接关掉所有线程,管你是不是空闲的,所以代码就直接全部interrupt掉

private void interruptWorkers() {
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    for (Worker w : workers)
      w.interruptIfStarted();
  } finally {
    mainLock.unlock();
  }
}

从SHUTDOWN或者STOP状态过度到TIDYING状态

上面的shutdown和shutdownNow函数最后都会执行tryTerminate()方法,这里就不贴代码了,直接口述一下:tryTerminate()不会直接关闭线程池,而是判断一下,当工作线程数为0,工作队列为空的时候,就把线程池的状态从SHUTDOWN或者STOP状态变为TIDYING状态,那么TIDYING状态其实就是我们可能需要执行一些钩子函数,比如在线程池关闭之前我们想关闭一些资源。这是他和TERMINATED状态的唯一区别

点赞是出好博客的绝佳动力

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-09 12:26:14  更:2022-05-09 12:28:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 23:44:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码