Java基础知识24--ThreadPoolExecutor线程池详细使用以及Java VisualVM监控线程使用情况
1.ThreadPoolExecutor概述
《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险;
-
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
-
CachedThreadPool 和 ScheduledThreadPool
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 线程池的工作过程
(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(推荐)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)