文章目录

  • 目标
  • 代码实践
  • 模拟线程卡死-代码
  • 请求发起类
  • 请求接受类
  • 分析
  • 优雅关闭线程的几种方式
  • 守护线程(不推荐)
  • 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线程正在运行,没有发现那个线程是阻塞的。

java排查线程卡住_java

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

优雅关闭线程的几种方式

守护线程(不推荐)

守护线程的机制在于当用户线程结束,守护线程也会自动结束,并退出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

状态一

正在运行业务代码

java排查线程卡住_java_02

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

状态二

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

java排查线程卡住_interrupt_03

状态三

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

java排查线程卡住_java排查线程卡住_04

依次看来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。

java排查线程卡住_java_05

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

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

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

            }

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

总结

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