并发编程之线程原理
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)