【并发编程】线程的基本原理和Thread Dump线程分析

线程的基本原理

放一张线程的原理图:
java代码创建线程后,我们通过调用start()方法启动线程,调用thread.cpp的方法来启动线程,底层还是通过操作系统的create_threadstart_thread方法来操作的线程
在这里插入图片描述

线程的运行状态

线程在运行过程中,会存在几种不同的状态,一般来说,在Java中,线程的状态一共是6种状态,分别是
NEW: 初始状态,线程被构建,但是还没有调用start方法
**RUNNABLED:**运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
**BLOCKED:**阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞
也分为几种情况

  • 等待阻塞:运行的线程执行wait方法,jvm会把当前线程放入到等待队列
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中
  • 其他阻塞:运行的线程执行Thread.sleep或者t.join方法,或者发出了I/O请求时,JVM会把当前线程设置为阻塞状态,当sleep结束、join线程终止、io处理完毕则线程恢复

WAITING: 等待状态
TIME_WAITING : 超时等待状态,超时以后自动返回
TERMINATED: 终止状态,表示当前线程执行完毕
在这里插入图片描述

如何中断线程

Thread提供了线程的一些操作方法,比如stop、suspend等,这些方法可以终止一个线程或者
挂起一个线程,但是这些方法都不建议大家使用。因为这些方法都是强制中断的方法,线程任务执行了一半被强制中断,可能会导致数据产生问题。这种行为类似于在linux系统中执行 kill -9命令,它是一种不安全的操作。

一般情形下线程是不需要用户手动去干预的, 哪些线程的中断需要外部干预呢?

  1. 线程中存在无限循环
  2. 线程中存在一些阻塞的操作,比如sleep、wait、join等

存在循环的线程中断

public class ThreadA extends Thread {

    public void run() {
        while(true){
            System.out.println("ThreadA .... ");
        }
    }

    public static void main(String[] args) {
        ThreadA myThread1 = new ThreadA();
        myThread1.start();
    }
}

对于线程中有无限循环的情形,代码中必须要有一个结束条件,并且用户可以在其他地方能够修改这个结束条件从而让线程感知到变化。像上面的代码,我们把while(true)改成while(flag),然后这个flag作为共享变量可以被外部修改,修改之后使得不满足循环条件然后退出循环并且结束线程

Java里提供了一个 interrupt 方法,这个方法就是实现线程中断操作的,其实现原理就是通过修改线程的中断标识来实现线程中断

public class InterruptDemo {

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                //默认情况下isInterrupted返回false、通过thread.interrupt变成了true
                i++;
            }
            System.out.println("final num:"+ i);
        },"InterruptDemo");
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }

}

处于阻塞状态的线程中断

public class InterruptDemo {

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    System.out.println("线程中断");
                    e.printStackTrace();
                    //当前异常只是响应了外部的中断命令,同时线程的中断状态也会复位,如果需要中断该线程,我们需要在这里再执行一次中断
                    Thread.currentThread().interrupt();
                }
            }
        },"InterruptDemo");
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }

}

我们平时在线程中使用的sleep、wait、join等操作,它都会抛出一个InterruptedException异常,为什么要抛出这个异常,是因为它在阻塞期间,必须要能够响应被其他线程发起的中断请求,而这个响应是通过InterruptedException 来实现的

InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如

  1. 直接捕获异常不做任何处理
  2. 将异常往外抛出
  3. 停止当前线程,并打印异常信息

Thread Dump线程分析

我们在使用线程的时候,如果出现问题,怎么排查?
比如说CPU占用率很高,响应很慢;CPU占用率不高,但响应很慢;线程出现死锁的情况

 nohup java -jar -Dserver.port=8580 thread-demo-0.0.1-SNAPSHOT.jar > localhost.log &

CPU不高,但响应很慢

出现死锁的时候就会有这个问题,因为两个线程彼此等待,所以虽然响应很慢,但是cpu并不高

   @GetMapping("/dead")
    public String dumpDeadLock(){
        Thread a = new ThreadA();
        Thread b = new ThreadB();
        a.start();
        b.start();
        return "ok";
    }
public class ThreadA extends Thread {
    @Override
    public void run() {
        synchronized (ThreadA.class) {
            System.out.println("thread A" + Thread.currentThread().getName());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (ThreadB.class) {
            }
        }
    }
}
public class ThreadB extends Thread {
    @Override
    public void run() {
        synchronized (ThreadB.class) {
            System.out.println("thread A" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (ThreadA.class) {
            }
        }
    }
}

查看死锁问题的操作步骤如下:

  1. 通过 jps 命令,查看java进程的pid
  2. 通过jstack 查看线程日志
    如果存在死锁情况,Thread Dump日志里面肯定会给出Found one Java-level deadlock:信息。只要找到这个信息就可以定位到问题。

在这里插入图片描述
在这里插入图片描述

CPU很高,且响应很慢

如果线程中出现死循环,就会有这样的问题

 @GetMapping("/loop")
    public String dumpWhile(){
        new Thread(new LoopThread()).start();
        return "ok";
    }
public class LoopThread implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("Thread run...");
        }
    }
}

排查cpu过高问题的步骤

  1. top -c 该命令可以动态显示进程及占用资源的排行榜,从而找到占用CPU最高的进程PID ,假设为PID=89831
  2. top -H -p [PID] 查找到该进程中最消耗CPU的线程,得到PID2=88765
  3. printf "0x%x\n" 88765 把对应的线程PID转化为16进制
  4. jstack 89831| grep -A 30 0x15abd 查看线程Dump日志,其中-A 30表示展示30行, 89831表示进程ID, 0x15abd表示16进制的线程ID

可以从打印的dump日志看出是哪个方法的执行逻辑导致的cpu过高


版权声明:本文为qq_35448165原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。