文章目录
- 目标
- 代码实践
- 模拟线程卡死-代码
- 请求发起类
- 请求接受类
- 分析
- 优雅关闭线程的几种方式
- 守护线程(不推荐)
- Future超时机制(推荐)
- 状态一
- 状态二
- 状态三
- Thread的interrupt 中断策略
- 尝试解决我们的问题(其实无法解决,具体看下列描述)
- 实践
- 请求接收类
- 请求发起类
- 测试
- 总结
目标
- 线上线程卡死问题排查
代码实践
模拟线程卡死-代码
程序环境: 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();
// 发现创建了一个 NewHttpClient
this.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.NetworkClient
var3 = 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: RUNNABLE
at 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
/**
* 用户线程,计时三秒,三秒后退出
*/
public class CheckThread extends Thread{
@SneakyThrows
@Override
public void run() {
// 仅允许3秒
Thread.sleep(3000);
}
}
--------------------------------------------------------------
/**
* 守护线程,作为工作线程
*/
public class DaemonThread extends Thread{
@SneakyThrows
@Override
public 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
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 {
// 可以得到一个返回结果,或者Void
Future<?> 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.TimeoutException
at java.util.concurrent.FutureTask.get(FutureTask.java:205)
at fun.gengzi.codecopy.business.problemsolve.future.Fun02FutureThread.main(Fun02FutureThread.java:32)
java.lang.InterruptedException: sleep interrupted
at 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;
@Override
public 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 exception
if (mayInterruptIfRunning) {
try {
Thread t = runner;
if (t != null)
// 当前线程不为空,就执行 interrupt 方法
t.interrupt();
} finally { // final state
UNSAFE.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{
@Override
public 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方法得到一个Future
Future<?> 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.TimeoutException
at 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超时情况,设置请求的超时时间(连接时间,获取响应超时时间)来主动的让线程释放中断。针对耗时长线程,在编程时注意避免死循环的出现,来防止出现线程一直存活,占用资源。当然,上述有一些我的一些拙见,如有错误,还请多多指正。