线程池运行任务后阻塞问题分析 您所在的位置:网站首页 线程池队列满了以后的策略 线程池运行任务后阻塞问题分析

线程池运行任务后阻塞问题分析

2024-07-05 11:15| 来源: 网络整理| 查看: 265

一、背景

今天有个朋友提了一个问题,模拟代码如下:

代码语言:javascript复制public class ThreadPoolDemo { public static void main(String[] args) { int nThreads = 10; ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(nThreads); executorService.execute(() -> System.out.println("test")); } }

运行结束后发现程序“阻塞”了。

可以看到程序还在运行中。

那么执行完毕为啥不退出?JVM在啥时候会退出?此程序为啥会阻塞,在哪个地方阻塞了呢?二、JVM退出的几种情况

JVM常见的退出原因有4种:

1、kill -9 pid  直接杀死进程

2、java.lang.System.exit(int status)

3、java.lang.Runtime.exit(int status)

4、没有非守护线程存活

三、分析

那么我们回到上面的问题,分析为啥程序没结束。

3.1 源码分析法

我们查看定长线程池的构造函数

java.util.concurrent.Executors#newFixedThreadPool(int)

代码语言:javascript复制 /** * Creates a thread pool that reuses a fixed number of threads * operating off a shared unbounded queue. At any point, at most * {@code nThreads} threads will be active processing tasks. * If additional tasks are submitted when all threads are active, * they will wait in the queue until a thread is available. * If any thread terminates due to a failure during execution * prior to shutdown, a new one will take its place if needed to * execute subsequent tasks. The threads in the pool will exist * until it is explicitly {@link ExecutorService#shutdown shutdown}. * * @param nThreads the number of threads in the pool * @return the newly created thread pool * @throws IllegalArgumentException if {@code nThreads { try { TimeUnit.SECONDS.sleep(20L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("test"); }; // 长度为10的定长线程池 int nThreads = 10; ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(nThreads); // 给线程池起个名字 executorService.setThreadFactory(new NamedThreadFactory("定长线程池")); // 执行两次任务(第二次执行时第一次还没结束) executorService.execute(runnable); executorService.execute(runnable); // 活跃线程数 System.out.println(executorService.getActiveCount()); }

注意为了效果更明显,这里让任务停顿了20秒钟,并给线程池起了个名字。

根据上面的知识点,我们推测一下流程:

主线程创建线程池,线程池执行第一个任务(和上面一样),线程池执行第二个任务(此时第一个线程sleep 20秒)由于未达到核心线程数10,因此会创建第二个线程来执行第二个任务,第二个任务也sleep 20秒,此时主线程打印线程池的活跃线程数(正在执行任务的线程)此时应该为2个。

结果和设想的一样。

那么我们我们如何看是该线程池否有两个线程呢?

3.2 JVM命令或工具

我们使用VisualVM查看该程序:

发现前我们创建两个线程先执行(时间可忽略)立即进入Sleeping ,然后Runnable状态然后执行(控制台打印了“test”,时间太短可界面都无法显示),然后进入WAITING状态

如图所示

通过线程dump我们可以看出线程从LinkedBlockingQueue取任务的时候阻塞了

java.util.concurrent.LinkedBlockingQueue#take

代码语言:javascript复制 public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }

在这一行:notEmpty.await();  将当前线程阻塞,底层用了java.util.concurrent.locks.LockSupport#park(java.lang.Object)。

感兴趣大家可以去看看 java.util.concurrent.locks.LockSupport#park(java.lang.Object)的用法和注释。

因此此线程池的两个核心线程一直存在并等待任务进入阻塞队列从而继续处理。

我们还可以再加一个任务来验证我的设想

代码语言:javascript复制 public static void main(String[] args) throws InterruptedException { // 定义一个任务 Runnable runnable = () -> { try { TimeUnit.SECONDS.sleep(20L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("test"); }; // 长度为10的定长线程池 int nThreads = 10; ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(nThreads); // 给线程池起个名字 executorService.setThreadFactory(new NamedThreadFactory("定长线程池")); // 执行两次任务(第二次执行时第一次还没结束) executorService.execute(runnable); executorService.execute(runnable); // 活跃线程数 System.out.println(executorService.getActiveCount()); TimeUnit.SECONDS.sleep(5L); executorService.execute(runnable); }

大家思考线程执行的状态,并通过VisualVM动态地观察效果。

通过上面的介绍我们知道,因为核心线程池不超时所以创建的核心线程一直存活,核心线程池阻塞的原因是从阻塞队列中取数据时被阻塞队列阻塞掉了。

由于有非守护线程一直存活所以虚拟机不会退出,因此程序也不会结束。

可能有人会说“线程池执行完任务都不会销毁的”,是吗?看看下面的例子:

那么我们再看一下下面的程序执行会怎样?

代码语言:javascript复制 public static void main(String[] args) throws InterruptedException { int nThreads =10; ThreadPoolExecutor executorService = (ThreadPoolExecutor)Executors.newFixedThreadPool(nThreads); // 允许核心线程池超时,超时时间为2s executorService.setKeepAliveTime(2L, TimeUnit.SECONDS); executorService.allowCoreThreadTimeOut(true); executorService.execute(()-> System.out.println("test")); }

执行后发现打印完test以后,等待2s没有任务,核心线程池的线程销毁,由于没有非守护线程,虚拟机退出(exit code 0)。

3.3 断点调试学习法

我们还可以通过断点来学习线程池的各种属性,并观察运行状态等。

代码语言:javascript复制 public static void main(String[] args) throws InterruptedException { // 定义一个任务 Runnable runnable = () -> { try { TimeUnit.SECONDS.sleep(20L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("test"); }; // 长度为10的定长线程池 int nThreads = 10; ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(nThreads); // 给线程池起个名字 executorService.setThreadFactory(new NamedThreadFactory("定长线程池")); // 执行两次任务(第二次执行时第一次还没结束) executorService.execute(runnable); executorService.execute(runnable); // 活跃线程数 System.out.println(executorService.getActiveCount()); }

我们在打印语句处断点,注意断点是只选择Thread:

否则会断住所有线程。

效果如下:

可以看到是否允许核心线程超时,完成的任务数,可以查看workers来查看工作的线程状态等。

还可以查看等待的条件和等待队列等信息:

学习并发可以多用调试,多种学习手段相结合,效果更好。

我们发现执行任务的线程被封装成了线程池的Worker对象:

代码语言:javascript复制 /** * Set containing all worker threads in pool. Accessed only when * holding mainLock. */ private final HashSet workers = new HashSet();

java.util.concurrent.ThreadPoolExecutor.Worker

继承自AbstractQueuedSynchronizer (AQS)并实现了Runnable接口。

感兴趣大家可以看源码,根据调试信息等深入学习。

四、总结我们要多从源码中学习知识,源码是最权威和全面的学习资料。我们要善用Java配套的工具,包括IDEAD的断点调试工具,JVM监控工具还有Java反编译和反汇编工具等。遇到问题多思考并且写DEMO验证。一个问题可以拓展出N个知识点,一个“小问题”的不理解,背后隐含着一串知识的不扎实,需要借机巩固。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有