Java基础知识24--ThreadPoolExecutor线程池详细使用以及Java VisualVM监控线程使用情况

1.ThreadPoolExecutor概述

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

Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。

  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

ThreadPoolExecutor 是 JDK 中的线程池实现,这个类实现了一个线程池需要的各个方法,它实现了任务提交、线程管理、监控等等方法。

创建线程池主要是 ThreadPoolExecutor 类来完成。

1.1  ThreadPoolExecutor 类的构造方法

ThreadPoolExecutor 类的构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

ThreadPoolExecutor中3 个最重要的参数:

(1) corePoolSize :核心线程数,定义了最小可以同时运行的线程数量。
(2) maximumPoolSize: 线程不够用时能够创建的最大线程数。
(3) workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

(4) keepAliveTime:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
(5) unit : keepAliveTime 参数的时间单位。

1.2 execute方法

execute方法是我们使用线程池的源头,我来看一下调用execute方法后干了什么

public void execute(Runnable command)

 

 重点看addWorker()方法,这里我们要联想到线程池是一个池子,从方法名也可以看出addWorker()增加一个工作者,那么这个Worker对象肯定会放到池子里面去,我们能想到这个池子肯定是一个集合,比如List,Set,Map都可以

 

 果然是用Set做集合,然后会把Worder对象放到Set中,然后我们再来看addWorker方法是怎么向线程池中添加任务的.

private boolean addWorker(Runnable firstTask, boolean core) 

 

 1.3 shutdown方法

当调用shutdown()方法时,线程池会等待正在执行任务的线程并且需要将阻塞队列中的任务执行完再进行销毁,并且不再接受新任务。跟shutdownNow()方法不同的是,shutdownNow方法不管是正在执行还是空闲的线程都会进行中断,返回阻塞队列中未完成的任务,阻塞队列中的元素也就不会再执行了

这里可能网友会有疑问了,怎么判断是空闲线程的???

我们可以看到interruptIdleWorkers方法,向线程发出中断信号前,需要获得tryLock()获取独占锁,才能执行t.interrupt()方法,我们再返过头来看runWorker()方法

可以看到如果getTask()返回不为null,则会执行任务,则会拿到独占锁,那么对于正在执行任务的线程是无法发出中断信息号的。除非任务执行完,释放锁。那么interruptIdleWorkers中的w.tryLock()才能拿到锁,发出中断信号

总的来说,ThreadPoolExecutor回收线程都是等getTask()获取不到任务,返回null时,调用processWorkerExit方法从Set集合中remove掉线程,getTask()返回null又分为2两种场景:

        1. 线程正常执行完任务,并且已经等到超过keepAliveTime时间,大于核心线程数,那么会返回null,结束外层的runWorker中的while循环

        2. 当调用shutdown()方法,会将线程池状态置为shutdown,并且需要等待正在执行的任务执行完,阻塞队列中的任务执行完才能返回null

1.4 线程池的工作过程

(1)线程池刚创建时,里面没有一个线程,任务队列作为参数传入。此时,即使任务队列中有任务,线程池也不会马上执行他们。

(2)当调用 execute() 方法添加一个任务时,线程池会做如下判断:

  • 如果正在运行的线程数 < corePoolSize,那么创建线程执行任务;

  • 如果正在运行的线程数 >= corePoolSize,那么将任务放入任务队列;

  • 如果队列满了,并且正在运行的线程数 < maximumPoolSize,那么将创建非核心线程执行任务;

  • 如果队列满了,并且正在运行的线程数 >= maximumPoolSize,那么线程池会抛出异常 RejectExecutionExcaption

(3)当一个线程完成任务后,在从队列中取出一个任务来执行

(4)如果一个线程空闲超过一定时间(keepAlivTime),线程池会判断,如果正在运行的线程数 > corePoolSize,则回收该线程。线程池在任务执行完后,线程数会维持在 corePoolSize 的大小。

2.ThreadPoolExecutor使用案例

ThreadPoolController.java

复制代码
/**
 * @Author lucky
 * @Date 2022/3/1 16:48
 */
@Slf4j
@RestController
@RequestMapping("/testThreadPool")
public class ThreadPoolController {
    private static final int CORE_POOL_SIZE=500;
    private static final int MAX_POOL_SIZE=600;
    private static final int QUEUE_CAPACITY=600;
    private static final Long KEEP_ALIVE_TIME=1L;

    @PostMapping("/uploadFileInner")
    public void uploadFileInner(@RequestParam int threadNum){
        //01 使用ThreadPoolExecutor创建线程池
        ThreadPoolExecutor executor=new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_CAPACITY));

        //02 创建指定threadNum个数的线程,利用CountDownLatch模拟并发
        final CountDownLatch start=new CountDownLatch(threadNum);
        for (int i = 0; i <threadNum ; i++) {
            ConcurrentUploadThread concurrentUploadThread=new ConcurrentUploadThread(start);
            executor.execute(concurrentUploadThread);
        }
        //03 关闭线程池
        executor.shutdown();
    }
}
复制代码

ConcurrentUploadThread.java

复制代码
/**
 * @Author lucky
 * @Date 2022/3/1 17:22
 */
@Slf4j
public class ConcurrentUploadThread implements Runnable {
    private final CountDownLatch startSignal;

    public ConcurrentUploadThread(CountDownLatch startSignal){
        this.startSignal=startSignal;
    }

    @Override
    public void run() {
        startSignal.countDown();
        log.info(Thread.currentThread().getName()+",prepare at:"+System.currentTimeMillis());
        try {
            startSignal.await();
            doTask();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void doTask() {
        try {
            Random random=new Random();
            int temp = random.nextInt(500);
            Thread.sleep(2000+temp);
            log.info(Thread.currentThread().getName()+"...............");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

Postman测试:

3. Java VisualVM 使用

VisualVM是JDK的一个集成的分析工具,自从JDK 6 Update 7以后已经作为Sun的JDK的一部分。在JDK1.6.07以上的版本中:到$JAVA_HOME/bin,点击jvisualvm.exe图标就可以启动VisualVM;

VisualVM可以做的:监控应用程序的性能和内存占用情况、监控应用程序的线程、进行线程转储(Thread Dump)或堆转储(Heap Dump)、跟踪内存泄漏、监控垃圾回收器、执行内存和CPU分析,保存快照以便脱机分析应用程序;同时它还支持在MBeans上进行浏览和操作。尽管 VisualVM自身要在JDK6以上的运行,但是JDK1.4以上版本的程序它都能被它监控。

3.1 查看本机所安装的JDK版本路径

本地安装路径:D:\software\jdk\jdk1.8.0_151\bin

3.2 Java VisualVM 使用案例

当我们启动一个springboot应用,此时,Java VisualVM会产生一个com.ttbank.flep.core.FileFlepApplication

(1) 内存分析

VisualVM 通过检测 JVM 中加载的类和对象信息等帮助我们分析内存使用情况,我们可以通过 VisualVM 的监视标签和 Profiler 标签对应用程序进行内存分析。

(2) CPU 分析

VisualVM 能够监控应用程序在一段时间的 CPU 的使用情况,显示 CPU 的使用率、方法的执行效率和频率等相关数据帮助我们发现应用程序的性能瓶颈。我们可以通过 VisualVM 的监视标签和 Profiler 标签对应用程序进行 CPU 性能分析。

(3) 线程分析

Java 语言能够很好的实现多线程应用程序。当我们对一个多线程应用程序进行调试或者开发后期做性能调优的时候,往往需要了解当前程序中所有线程的运行状态,是否有死锁、热锁等情况的发生,从而分析系统可能存在的问题。

参考文献:

https://blog.csdn.net/qielanyu_/article/details/115614762(记一次排查服务器线程数异常的过程:IdleConnectionEvictor导致线程数持续增加)

https://www.cnblogs.com/semi-sub/p/13908099.html(推荐)

 

posted @   雨后观山色  阅读(657)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示