文章目录

  • 目标
    • 代码实践
      • 模拟线程卡死-代码
      • 请求发起类
      • 请求接受类
      • 分析
    • 优雅关闭线程的几种方式
      • 守护线程(不推荐)
      • Future超时机制(推荐)
        • 状态一
        • 状态二
        • 状态三
      • Thread的interrupt 中断策略
    • 尝试解决我们的问题(其实无法解决,具体看下列描述)
      • 实践
        • 请求接收类
        • 请求发起类
        • 测试
    • 总结

目标

  • 线上线程卡死问题排查

    参考: Java并发编程(第十章,第四章)

    Future.get卡死,线程池的一个坑点 可以参考,注意点

    FutureTask的cancel方法真的能停止掉一个正在执行的异步任务吗 可以看下

    一个线程中断引发Bug的“爆肝”排查经历

    Java里一个线程调用了Thread.interrupt()到底意味着什么?

    java线程池线程超时关闭的两种我认为比较好的方式

    【Java】Java多线程任务超时结束的5种实现方法

    Java多线程任务超时结束的5种实现方法

    一条慢SQL导致购物车服务无法使用的解决方案

承接上篇 并发编程-线程卡死问题排查与解决

代码实践

模拟线程卡死-代码

程序环境: JDK8

设计思路:请求发起类,模拟发送一个请求资源服务.

请求就直接采用 new URL(…).openStream() 发起请求。

请求接受类,模拟接受一个请求,但是不返回响应。

本质的http请求,都要解析为Socket形式,所以使用Socket模拟服务端即可。

请求发起类

public class InOrOutPutStream {public static void main(String[] args) {InputStream inputStream = null;try {inputStream = new URL("http://localhost:8080").openStream();} catch (IOException e) {e.printStackTrace();}// 这里使用hutool工具类,将流写入文件  可以忽略// FileWriter fileWriter = FileWriter.create(new File("D:\\ideaworkspace\\1.jpg"));// File file = fileWriter.writeFromStream(inputStream);}
}

请求接受类

// 请求接受类
public class BootStrap {// 监听端口
public static final int PORT = 8080;
public static void main(String[] args) {ServerSocket serverSocket = new ServerSocket(PORT);while (true) {Socket socket = serverSocket.accept();//TODO  不返回,直接卡死Thread.sleep(2000000);}}
}

运行测试,会发现请求发起类一直在等待… 模拟线程卡死的情况

分析下源码: new URL(…).openStream()

HttpURLConnection 连接对象connect(); //连接方法plainConnect();// 发现创建了一个 NewHttpClientthis.http = this.getNewHttpClient(this.url, this.instProxy, this.connectTimeout);this.http.setReadTimeout(this.readTimeout);
NewHttpClient// 在构造方法中有一个 openServer()new HttpClient(var0, var1, var2)openServer()                         doConnect()                              sun.net.NetworkClientvar3 = new Socket(Proxy.NO_PROXY);   sun.net.NetworkClient  // 就是new了一个 Socket

本质上就是一个 socket请求。

分析

使用JVM 提供的命令和工具,来分析下运行的程序情况。

使用JPS 来查找当前运行的java程序进程id

D:\ideaworkspace\codecopy\codecopy>jps
15568 -- process information unavailable
24632 BootStrap
8328 Example
7820 Jps-- 只关注我们运行的程序 24632 BootStrap  8328 Example

使用jstack 获取当前进程id 的堆栈情况,看是否有死锁情况

D:\ideaworkspace\codecopy\codecopy> jstack -l 8328
2021-06-19 11:28:09
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode):
--------------- 忽略一部分日志 ---------"main" #1 prio=5 os_prio=0 tid=0x0000000003649800 nid=0x2e80 runnable [0x000000000363e000]java.lang.Thread.State: RUNNABLEat java.net.SocketInputStream.socketRead0(Native Method)at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)at java.net.SocketInputStream.read(SocketInputStream.java:170)at java.net.SocketInputStream.read(SocketInputStream.java:141)at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)at java.io.BufferedInputStream.read1(BufferedInputStream.java:286)at java.io.BufferedInputStream.read(BufferedInputStream.java:345)- locked <0x000000076c3c7a90> (a java.io.BufferedInputStream)at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:704)at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:647)at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1569)- locked <0x000000076c3a9a70> (a sun.net.www.protocol.http.HttpURLConnection)at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)- locked <0x000000076c3a9a70> (a sun.net.www.protocol.http.HttpURLConnection)at java.net.URL.openStream(URL.java:1045)at fun.gengzi.codecopy.business.problemsolve.thread.Example.main(Example.java:12)Locked ownable synchronizers:- None
JNI global references: 321-- 没有发现死锁信息

再使用jconsle 分析,发现main线程正在运行,没有发现那个线程是阻塞的。

从上述可知,线程将一直假死下去,下面来探讨下如何在判断超时的情况关闭线程。

优雅关闭线程的几种方式

守护线程(不推荐)

守护线程的机制在于当用户线程结束,守护线程也会自动结束,并退出jvm。

可以创建一个用户线程作为计时线程,设置线程休眠固定的时间。 设置守护线程为工作线程,执行业务,当超过设置的时间,用户线程执行结束,随后守护线程也进入结束。

缺点:守护线程作用于整个jvm,无法控制某一个用户线程的结束,就指定关闭守护线程。仅在演示代码中,可以体现价值。

代码演示:JDK1.8

代码参考:daemon

/**
* 用户线程,计时三秒,三秒后退出
*/
public class CheckThread  extends Thread{@SneakyThrows@Overridepublic void run() {// 仅允许3秒Thread.sleep(3000);}
}--------------------------------------------------------------
/**
* 守护线程,作为工作线程
*/
public class DaemonThread extends Thread{@SneakyThrows@Overridepublic void run() {// 执行业务代码 假设需要 5000 秒Thread.sleep(5000000);}
}--------------------------------------------------------------/**
* <H1>使用守护线程解决线程超时问题</H1>
* 守护线程执行业务代码,用户线程控制时间,当用户线程完毕,守护线程会自动退出
* <p>
* 需要两个线程
* 当所有的非守护线程退出后,整个JVM 的进程就会退出,
*
* 这个方法不可能用于生产环境,条件过于苛刻,当所有的非守护线程退出后,整个JVM 进程才会退出。一个系统包含了很多线程,执行不同业务,用户线程不只一个。
*
* @author gengzi
* @date 2021年5月31日13:54:40
*/
public class Fun01DaemonThread {public static void main(String[] args) {DaemonThread deprecated = new DaemonThread();// 设置为守护线程deprecated.setDaemon(true);deprecated.start();CheckThread checkThread = new CheckThread();checkThread.start();}
}

当执行CheckThread 执行完成后,会发现main方法结束,守护线程退出

Future超时机制(推荐)

使用Future超时机制,提供了阻塞的get方法,当时间超过时效,会停止当前运行的线程。主要在异步流程下使用。

代码演示:JDK1.8

代码参考:future


public class Fun02FutureThread {// 创建线程池,注意这里将 核心线程(corePoolSize) 设置为 0 ,主要用于体现当没有使用线程池线程时,不会有线程存在于进程中public static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, 6,60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(50));public static void main(String[] args) throws InterruptedException {// 可以得到一个返回结果,或者VoidFuture<?> future = threadPoolExecutor.submit(() -> {try {for (int i = 0; i < 1000; i++) {System.out.println("执行" + i);Thread.sleep(1000);}} catch (InterruptedException e) {e.printStackTrace();}});try {// 超时阻塞,当执行时间超过2秒,将会抛出 TimeoutException 异常future.get(2, TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();// TODO 必须加return; // 以此来中断线程} catch (ExecutionException e) {e.printStackTrace();} catch (TimeoutException e) {e.printStackTrace();// 中断超时线程// 当超时后,中断执行此任务线程的方式来试图停止任务,返回true 说明中断成功boolean cancel = future.cancel(true);System.out.println("中断状态" + cancel);} finally {// 线程池退出System.out.println("销毁线程池");// threadPoolExecutor.shutdownNow();}// 不让main方法退出,便于观察执行结果Thread.sleep(50000000);}
}

先看执行日志,从日志看,当业务线程执行时间超过2秒会抛出 TimeoutException 超时异常,但是这里注意超时后,执行线程并不会中断,需要手动中断

执行0
执行1
java.util.concurrent.TimeoutExceptionat java.util.concurrent.FutureTask.get(FutureTask.java:205)at fun.gengzi.codecopy.business.problemsolve.future.Fun02FutureThread.main(Fun02FutureThread.java:32)
java.lang.InterruptedException: sleep interruptedat java.lang.Thread.sleep(Native Method)at fun.gengzi.codecopy.business.problemsolve.future.Fun02FutureThread.lambda$main$0(Fun02FutureThread.java:23)at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)at java.util.concurrent.FutureTask.run(FutureTask.java:266)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)at java.lang.Thread.run(Thread.java:748)
中断状态true
销毁线程池

使用jconsle 来分析线程运行情况,但是修改一下超时时间为 50 秒,方便看结果。

这里设置线程池的核心线程数为0的目的,就是如果当前线程中没有线程被使用,在超过存活时间后(60S),就会被收回,便于演示。

请关注线程池中的pool-1-thread-1

状态一

正在运行业务代码

注意这里线程状态是 timed_wating (通常是调用了sleep(long)或者wait(long)方法), 因为我们在方法中使用了 sleep 来休眠,所以在检测页面可能展示的就是 timed_wating。

状态二

业务代码超时,pool-1-thread-1 被中断后,会回到线程池中等待其他业务调用。可以发现现在的线程状态是 Conditon上的TIME_WAITING

状态三

核心线程超过超时时间被回收,这里注意,我们设置了此线程池的核心线程数是0,也就是当没有线程被使用,并且超过了超时时间,就会被JVM回收。

依次看来Future的get(timeout)超时阻塞方法可以实现对超时线程的检测

Thread的interrupt 中断策略

或者设置一个信号标志(flag)

主要解决循环线程的中断,Thread 的 interrupt 可以设置中断标志,唤醒轻量级阻塞线程。当某个线程被 wait() 或者 sleep() 未超过时间,使用 interrupt 会唤醒阻塞线程,抛出interruptException 执行异常处理流程。但是在常规业务中,可能也不会一直循环执行某项业务,不过需要了解下。

代码演示:JDK1.8

代码参考:


/**
* 循环线程,中断
*/
public class ThreadInterrupt {public static void main(String[] args) throws InterruptedException {Runner runner = new Runner();Thread thread = new Thread(runner);thread.start();Thread.sleep(5000);// 中断线程,这里的中断,并不是jvm真正中断线程,只是未此线程增加了一个 中断标志(true)// 具体中断线程逻辑需要自己实现thread.interrupt();// 调用取消方法,来实现中断// runner.cancel();}static class Runner implements Runnable {private volatile int i = 0;// 信号标志private volatile boolean flag = true;@Overridepublic void run() {// 检测到线程中断状态为 true 或者 信号标志为 false,就退出while (flag && !Thread.currentThread().isInterrupted()) {System.out.println("打印:" + i++);
//                try {//                    Thread.sleep(1000);
//                } catch (InterruptedException e) {//                    e.printStackTrace();// 如果触发了异常,如果要中止线程,请再后面加 return; 返回,否则当前线程不会中断
//                    return;
//                }}}/*** 中断*/private void cancel() {flag = false;}}
}

特别注意:对于中断的线程,必须再捕获InterruptedException return出去,这样才能终止。否则循环线程还是会继续执行

尝试解决我们的问题(其实无法解决,具体看下列描述)

综上所述,应该选择Future 的超时机制或者 Thread 的interrupt 中断策略来解决Socket超时的问题,但是对于socket超时或业务执行时间过长(循环时间过长)的这种线程假死情况,根本无法用中断关闭线程。因为上述Future 的cancel()内部实现本质就是使用Thread.interrupt() 来中断线程的,我们又知道interrupt()方法仅仅只是增加了中断标记,具体的中断逻辑需要程序员自行编写。对于线程中不触发中断操作的业务逻辑(或者当前线程没有sleep(long) wait(long)这种超时阻塞的),根本无法停止(或者抛出InterruptException)

源码分析

boolean cancel = future.cancel(true);// -- 源码
public boolean cancel(boolean mayInterruptIfRunning) {if (!(state == NEW &&UNSAFE.compareAndSwapInt(this, stateOffset, NEW,mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))return false;try {    // in case call to interrupt throws exceptionif (mayInterruptIfRunning) {try {Thread t = runner;if (t != null)// 当前线程不为空,就执行 interrupt 方法t.interrupt();} finally { // final stateUNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);}}} finally {finishCompletion();}return true;}

实践

模拟Socket超时未响应的代码,从代码中具体来分析

代码演示:JDK1.8

请求接收类
// 请求接受类
public class BootStrap {// 监听端口public static final int PORT = 8080;public static void main(String[] args) throws IOException, InterruptedException {ServerSocket serverSocket = new ServerSocket(PORT);while (true) {Socket socket = serverSocket.accept();//TODO  不返回,直接卡死Thread.sleep(2000000);}}
}
请求发起类

使用Future模式,来设置超时中断逻辑

线程类

public class Runner implements Runnable{@Overridepublic void run() {InputStream inputStream = null;try {inputStream = new URL("http://localhost:8080").openStream();} catch (IOException e) {e.printStackTrace();}}
}

业务类

public class Fun02FutureThread {// 线程池,但是我这次设置了 超时时间为 1毫秒,只有执行完任务,没有再次使用该线程就回收public static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, 6,1, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(50));public static void main(String[] args) throws InterruptedException {// 创建工作线程Runner runner = new Runner();// 使用submit方法得到一个FutureFuture<?> future = threadPoolExecutor.submit(runner);try {// 仅阻塞20毫秒,为了演示future.get(20, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {e.printStackTrace();System.out.println("中断线程");// TODO 必须加return;return;} catch (ExecutionException e) {e.printStackTrace();} catch (TimeoutException e) {e.printStackTrace();// 中断执行此任务线程的方式来试图停止任务,返回true 说明成功boolean cancel = future.cancel(true);System.out.println("中断状态" + cancel);} finally {System.out.println("销毁线程池");long taskCount = threadPoolExecutor.getTaskCount();System.out.println("线程池已安排执行的大致任务总数:"+taskCount);// threadPoolExecutor.shutdownNow();}// 保持现场Thread.sleep(50000000);}
}
测试

运行两个main方法即可。

从日志看,触发了超时异常,执行了中断线程操作,但是为什么最后finally中,现在已经执行的任务总数是1。

java.util.concurrent.TimeoutExceptionat java.util.concurrent.FutureTask.get(FutureTask.java:205)at fun.gengzi.codecopy.business.problemsolve.future.Fun02FutureThread.main(Fun02FutureThread.java:35)
中断状态true
销毁线程池
线程池已安排执行的大致任务总数:1

这样还不是很能说明问题,看一下jconsole中是否还存在该线程,发现该线程状态依然是Runnable。

其实也就是上面说的,Future 的cancel 内部实现本质就是使用thread.interrupt() 来中断线程的。这里没有触发中断的逻辑,也就不会出现中断线程的情况。类似的循环时间长的,也不会被中断。

代码示例:可以将线程类中的代码更换为如下逻辑,

          for(;;){// 这里是死循环,可以设置为次数很多次的,其实也不会被中断if((i++) % 100000000 == 0){System.out.println("test");System.out.println(Thread.currentThread().isInterrupted());}}

那我们不是可以自行实现中断逻辑吗?其实在日常业务中,我们很少会使用到Thread.interrupt()或者设置信号标志的形式来中断循环执行的业务线程,因为这种业务场景很少,而且如果贸然使用不加一小心,会留下很多问题。

总结

通过测试和实践,发现通过检测线程是否超时来关闭线程仅在可中断的场景下使用,对于类似Socket超时或者无法加入中断逻辑的线程,无法使用这种形式。所以针对Socket超时情况,设置请求的超时时间(连接时间,获取响应超时时间)来主动的让线程释放中断。针对耗时长线程,在编程时注意避免死循环的出现,来防止出现线程一直存活,占用资源。当然,上述有一些我的一些拙见,如有错误,还请多多指正

听说点赞关注的人,身体健康,一夜暴富,升职加薪迎娶白富美!!!

点我领取每日福利
微信公众号:耿子blog
GitHub地址:gengzi

并发编程-线程卡死问题实践相关推荐

  1. 判断线程是否执行完毕_Java并发编程 | 线程核心机制,基础概念扩展

    源码地址:GitHub || GitEE 一.线程基本机制 1.概念描述 并发编程的特点是:可以将程序划分为多个分离且独立运行的任务,通过线程来驱动这些独立的任务执行,从而提升整体的效率.下面提供一个 ...

  2. 高并发编程-线程通信_使用wait和notify进行线程间的通信2_多生产者多消费者导致程序假死原因分析

    文章目录 概述 jstack或者可视化工具检测是否死锁(没有) 原因分析 概述 高并发编程-线程通信_使用wait和notify进行线程间的通信 - 遗留问题 我们看到了 应用卡住了 .... 怀疑是 ...

  3. python 线程同步_Python并发编程-线程同步(线程安全)

    Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...

  4. 并发编程——线程协作

    并发编程--线程协作 ​ 前面学习了线程,那么并发编程中,如何协调多个线程来开发呢? Semaphore ​ 信号量跟前面将的同步互斥解决方案--信号量是一个东西,这是JDK的信号量实现. 源码分析 ...

  5. Java 并发编程 -- 线程池源码实战

    一.概述 小编在网上看了好多的关于线程池原理.源码分析相关的文章,但是说实话,没有一篇让我觉得读完之后豁然开朗,完完全全的明白线程池,要么写的太简单,只写了一点皮毛,要么就是是晦涩难懂,看完之后几乎都 ...

  6. java线程池_Java 并发编程 线程池源码实战

    作者 | 马启航 杏仁后端工程师.「我头发还多,你们呢?」 一.概述 笔者在网上看了好多的关于线程池原理.源码分析相关的文章,但是说实话,没有一篇让我觉得读完之后豁然开朗,完完全全的明白线程池,要么写 ...

  7. 并发编程线程通信之管道流

    前言 在并发编程中,需要处理两个问题:线程之间如何通信及线程之间如何同步.通知是指线程之间以何种机制来交换信息.在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递. 在共享内存的并发模型里, ...

  8. C++并发编程线程间共享数据std::future和sd::promise

    线程间共享数据 使用互斥锁实现线程间共享数据 为了避免死锁可以考虑std::lock()或者boost::shared_mutex 要尽量保护更少的数据 同步并发操作 C++标准库提供了一些工具 可以 ...

  9. 并发编程——线程——锁

    并发编程中避免不了在同一时间对同一数据的更改,因此,对锁的使用变得尤为重要,什么时间.什么场景该用什么类型的锁都是有讲究的,接下来介绍几种常见的锁. 死锁现象 问题产生需求,在学新的锁之前先来看看我们 ...

  10. Java并发编程—线程间协作方式wait()、notify()、notifyAll()和Condition

    原文作者:Matrix海 子 原文地址:Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 目录 一.wait().notify()和notifyA ...

最新文章

  1. 高精地图与自动驾驶(下)
  2. 20110125 学习记录:在SQL Server 2005数据库中修改存储过程
  3. Spark编程基础(Python版)
  4. vsphere ha 虚拟机监控错误_学会这3招,分分钟迁移业务繁忙虚拟机!
  5. 【CyberSecurityLearning 44】iptables包过滤与网络地址转换
  6. pdf各种处理 PDF 的实用代码:PyPDF2、PDFMiner、pdfplumber
  7. Google:2-1 tfkeras简介
  8. Java 容器的使用及数组、List、Set 的相互转换
  9. 类、匿名类、静态、构造、单例
  10. Js中apply和Math.max()函数的问题及区别
  11. 常见的重要电脑英语及缩写
  12. 关于fftshift引发的问题与思考
  13. ffmpeg命令操作 合并视频 取图片帧数 获取音频
  14. 新书《算法竞赛》已出版
  15. 电压基准和稳压电源-BUCK\BOOST原理讲解
  16. 记录uni-app的时间选择器
  17. PyTorch RuntimeError: size mismatch, m1:
  18. 课设复习之信息论固定算术编码与译码
  19. 域控制器中五个角色基础(必记)
  20. LoRa节点开发:4、代码详解 LoRaWAN节点入网

热门文章

  1. JavaScript(3)基础
  2. java中的URLEncoder和URLDecoder类
  3. JavaScript高级编程——BOM
  4. qt实现涂鸦板_Qt涂鸦板程序图文详细教程
  5. php fpm 报错,php-fpm报错
  6. 给钱,才是真的对你好
  7. 命名实体消歧的代码实现
  8. w7计算机文件夹打开怎么设置密码,怎样设置文件夹密码 Win7系统文件夹加密步骤详解...
  9. 物质环境、符号还是认知模型?——谈韩礼德、马丁、范迪克的语境观
  10. IDEA 2019 激活码(注册码)