1.并发编程之线程原理

本文详细解读了Java线程的启动原理、状态转换,特别关注了线程终止的正确方式,中断方法interrupt及其在阻塞状态下的应用。还介绍了如何通过ThreadDump日志诊断CPU问题和死锁,为并发编程实践者提供实用技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并发编程之线程原理

1 多线程的基本原理

线程的start方法,实际上底层做了很多事情,具体的实现简图如下:

OS调度算法有很多,比如先来先服务调度算法(FIFO)、最短优先(对短作业的优先调度)、时间片轮转调度等。
请添加图片描述

1.1 线程的状态

线程在运行过程中,会存在几种不同的状态,一般来说,在Java中线程状态一共分为6中,分别是:

  • NEW:初始状态,线程被构建,但是还没有调用start方法。
  • RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态,统一称为“运行中”。
  • BLOCKED:阻塞状态,表示线程进入等待状态,即线程因为某种原因放弃了CPU使用权,阻塞也分为几种情况:
    • 等待阻塞:运行的线程执行wait方法,jvm会把当前线程放入到等待队列。
    • 同步阻塞:运行的线程在获取对象的同步锁时,如果该同步锁被其他线程占用了,那么jvm会把当前的线程放入到锁池中。
    • 其他阻塞:运行的线程执行Thread.sleep或者t.join方法,或者发出了I/O请求时,jvm会把当前的线程设置为阻塞状态,当sleep结束、join线程终止、I/O处理完毕,线程恢复。
  • WAITNG:等待状态。
  • TIME_WAITING:超时等待状态,超时以后自动返回。
  • TERMINATED:终止状态,表示当前线程执行完毕。
    请添加图片描述

1.2 线程的终止

如何正确的停止一个线程?

我们知道Thread给我们提供了一些操作方法,比如stop、suspend等,这些方法可以终止一个线程或者挂起一个线程,但是这些方法不建议使用。

举个例子,假设一个线程中,有多个任务正在执行,此时,如果调用stop方法去强行中断,那么这个时候相当于是发送一个指令给操作系统将这个线程结束掉,但是操作系统这个动作完成不代表线程中的这个任务执行完成,和可能出现线程中的任务执行了一半被强制中断,最终产生数据相关问题。这种行为类似于linux系统中的kill -9,他是一种不安全的操作。

那么除了这种方式以外,还有什么方式可以实现线程的终止呢?要了解这个问题,我们首先需要知道,一个线程什么情况下算是终止了。

1.2.1 一个线程在什么情况下算执行结束

分析下面这段代码,通过start()方法启动一个线程后,本质上就是执行这个线程的run()方法。那么这个线程的run()方法执行结束之前,一直处于运行中状态,直到run()方法中的指令执行完毕,那么这个线程就会被摧毁。

public class MyThread extends Thread {
  public void run() {
		System.out.println("MyThread run");
  }
}
MyThread myThread = new MyThread();
myThread.start();

在正常情况下,这个线程是不需要人为干预去结束的,如果强制结束,只能走stop方法。

在哪些情况下,线程的中断需要外部干预呢?

  • 线程中存在无限循环执行,比如while(true)循环。
  • 线程中存在一些阻塞的操作,比如sleep、wait、join等。
1.2.2 存在循环的线程

假设存在如下场景,在run()方法中,存在一个while循环,因为这个循环的存在使得线程的run()方法一直无法结束,这种情况下,如何终止线程呢?

public class MyThread extends Thread {
    public void run() {
        while(true) {
        System.out.println("MyThread run");
        }
    }
}
MyThread myThread = new MyThread();
myThread.start();

按照我们开发的思维,首先要解决的是,while(true)这个循环必须要有一个结束条件,其次是在其他地方要能够修改这个结束条件,让这个线程感知到变化。假设我们把while(true)改成while(flag),这个flag可以作为共享变量被外部修改,修改之后使得循环条件无法满足,从而退出循环结束这个线程。

这段逻辑其实非常简单,就是给了线程一个退出的条件,如果没有这个条件,线程会一直执行下去。

实际上,在Java中提供了一个interrupt()方法,这个方法就是实现线程中断操作的,他的作用与上面案例的作用一样。

1.2.3 interrupt方法

当其他线程调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。

线程通过检查自身是否中断来进行响应,可以通过isInterrupted()来判断是否中断。

private static int i;
public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(()->{
        //默认情况下isInterrupted返回的是false,通过thread.interrupt变成了true
        while(!Thread.currentThread().isInterrupted()) {
            i++;
        }
        System.out.println("i="+i);
    },"interruptDemo");
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    thread.interrupt();
}

这种通过标识位中断的方式,能够使得线程在终止时有机会去清理资源,而不是直接将线程停止,这种终止线程的方法显得更加安全和优雅。

1.2.4 处于阻塞状态下的线程中断

另一种情况,就是当线程处于阻塞状态下时,我们想要中断这个线程,需要怎么做呢?

public class InterruptDemo {
    private static int i;
  	public static void main(String[] args) throws InterruptedException {
    		Thread thread=new Thread(()->{
    				while(!Thread.currentThread().isInterrupted()){
        				try {
            				TimeUnit.SECONDS.sleep(1);
        				} catch (InterruptedException e) {
            				e.printStackTrace();
								}
            }
    				System.out.println("Num:"+i);
				},"interruptDemo");
				thread.start();
				TimeUnit.SECONDS.sleep(1);
				thread.interrupt();
    }
}

从这个例子中反馈出一个问题,在线程中使用sleep、wait、join等操作,它就会抛出一个InterruptedException异常。为什么会抛出这个异常,因为它下阻塞期间,必须要能够响应被其他线程发起中断请求之后的一个响应,而这个响应是通过InterruptedException来体现的。

但是这里需要注意的是,在这个异常中如果不做任何处理的话,我们无法去中断这个线程的,因为当前的异常只是响应了外部对于这个线程的中断命令,同时,线程的中断状态也会复位,如果需要中断,则需要在catch中添加下面代码。

public class InterruptDemo {
    private static int i;
  	public static void main(String[] args) throws InterruptedException {
    		Thread thread=new Thread(()->{
    				while(!Thread.currentThread().isInterrupted()){
        				try {
            				TimeUnit.SECONDS.sleep(1);
        				} catch (InterruptedException e) {
            				e.printStackTrace();
                  	//再次中断
                  	Thread.currentThread().interrupt()
								}
            }
    				System.out.println("Num:"+i);
				},"interruptDemo");
				thread.start();
				TimeUnit.SECONDS.sleep(1);
				thread.interrupt();
    }
}

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

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

2 Thread Dump日志分析

我们在使用线程的时候,如果出现问题,怎么排查?比如:

  • CPU占用率很高,响应很慢
  • CPU占用率不高,响应很慢
  • 线程出现死锁情况

2.1 CPU占用率不高,响应很慢

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

  • 通过jps命令,查看java进程的pid
  • 通过jstack查看线程日志

如果存在死锁情况,Thread Dump日志里面肯定会给出Found one Java-level deadlock信息。只要找到这个信息就可以立马定位到问题并去解决。

 Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x0000000026070c88 (object 0x00000007163b7d78, a
java.lang.Integer),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00000000260735c8 (object 0x0000000716649aa8, a
java.lang.Integer),
  which is held by "Thread-1"

2.2 CPU占用率很高,响应很慢

有时候我们会发现CPU占用率很高,系统日志也看不出问题,这种情况下,我们需要去看下运行的线程有没有异常。

  • 通过top -c动态显示进程及占用资源的排行榜,从而找到占用CPU最高进程的PID,得到PID=80972。

    PID USER        PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    80972 root      20   0 7773456 296124  12904 S 100.2  1.8   0:38.83 java
    
  • 然后再定位到对应的线程,top -H -p 80972,查到该进程中最消耗CPU的线程,得到PID=81122。

    PID USER        PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
    81122 root      20   0 7773456 258504  12932 R 99.8  1.6   5:56.34 java
    80972 root      20   0 7773456 258504  12932 S  0.0  1.6   0:00.00 java
    
  • 通过printf “0x%x\n” 81122命令,把对应的线程PID转化为16进制。

    [root@localhost xzz]# printf "0x%x\n" 81122
    0x13ce2
    
  • 执行命令jstack 80972 | grep -A 20 0x13ce2查看线程Dump日志。

    [root@localhost xzz]# jstack 80972 | grep -A 20 0x13ce2
    "Thread-3" #30 daemon prio=5 os_prio=0 tid=0x00007f84500ce000 nid=0x13ce2
    runnable [0x00007f84a78f7000]
       java.lang.Thread.State: RUNNABLE
        at java.io.FileOutputStream.writeBytes(Native Method)
        at java.io.FileOutputStream.write(FileOutputStream.java:326)
        at
    java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
        at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
        - locked <0x00000006c812f1b0> (a java.io.BufferedOutputStream)
        at java.io.PrintStream.write(PrintStream.java:482)
        - locked <0x00000006c812f190> (a java.io.PrintStream)
        at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
        at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
        at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
        - locked <0x00000006c812f2d0> (a java.io.OutputStreamWriter)
        at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
        at java.io.PrintStream.newLine(PrintStream.java:546)
        - eliminated <0x00000006c812f190> (a java.io.PrintStream)
        at java.io.PrintStream.println(PrintStream.java:807)
        - locked <0x00000006c812f190> (a java.io.PrintStream)
        at com.example.threaddemo.WhileThread.run(ThreadController.java:33)
        at java.lang.Thread.run(Thread.java:748)
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值