并发编程——Thread
线程、进程、CPU
要想研究并发编程首先得了解线程、进程、CPU他们三个之间的联系
- 线程:线程是操作系统调度的最小执行单位,是程序执行的一条执行路径。一个进程可以包含多个线程,它们共享同一进程的上下文和资源。线程之间可以并发执行,通过上下文切换来实现多个线程之间的切换。
- 进程:进程是操作系统中的一个运行实例,它具有独立的内存空间和系统资源。一个进程可以包含多个线程,这些线程共享同进程的上下文和资源。进程之间相互独立,有独立的内存空间和系统资源,通过操作系统的调度来实现进程之间的切换。
- CPU:CPU(中央处理器)是计算机的主要执行部件,负责执行指令和处理计算任务。线程和进程都需要依赖CPU来执行指令和完成计算任务。操作系统通过调度算法来管理和分配CPU资源,将CPU的时间片分配给不同的进程或线程进行执行。
可以简单的理解为 一个进程里边包含多个线程,进程代表的就是电脑上启动的软件,统一都是由cpu通过切换时间片来进行调度。再举个例子,进程就好比一个工厂里的生产线,它有生产任务。线程就好比生产线上的工人,它们一起协作才能完成生产任务。每个工人(线程)干的活也不一样。那cpu就像老板,将不同的工序分配给不同的工人。总的来说通过合理地组织进程和线程的分工,并充分利用CPU的计算能力,可以有效地提高计算机的处理效率。
java中线程的使用
java中的线程使用非常简单,代码如下
extends Thread
public class ThreadOne extends Thread {
public static void main(String[] args) {
Thread t = new ThreadOne();
t.start();
}
@Override
public void run() {
System.out.println("执行任务");
}
}
implements Runnable
public class ThreadTwo implements Runnable {
public static void main(String[] args) {
ThreadTwo t = new ThreadTwo();
new Thread(t).start();
}
@Override
public void run() {
System.out.println("执行任务");
}
}
线程的工作原理
我们已经了解了线程、进程、CPU之间的关系,也知道了在java中线程的使用,那么线程时怎么执行的,或者说在JVM中是怎么体现的呢
这个地方只能进行推导一下加深理解(因为具体会涉及到操作系统底层C++代码,看不懂超纲了)
-
假设我们通过 new Thread().start() 来执行一个线程,那么它肯定是在操作系统底层创建一个线程,然后再去执行start()方法,Java里面的Thread类在JVM上对应的文件是thread.cpp。在thread.cpp文件中定义了很多和线程相关的方法,Thread.java类中的native方法就是在thread.cpp中实现的。
-
创建完线程调用start()方法后线程并不是立即执行,而是处在一个就绪状态,等着CPU来进行调度(就好比新员工刚来公司等着领导安排任务)
-
通过一系列调度算法来确定让哪个线程来执行,比如说刚好轮到 我们的 new Thread() 这个线程 ,回调它的run() 方法
大致含义如下图所示
线程的状态(生命周期)
线程的执行有开始就会有结束,我们也都知道run()方法执行完,线程就终止了,那么问题来了,在这个过程中线程会有哪些状态,首先我们通过Thread源码中可以看到线程的6种状态
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
NEW(初始)
初始化的状态就不用详细说了 new Thread() 线程被构建
RUNNABLE(运行)
运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中
BLOCKED(阻塞)
阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞 也分为几种情况
- 等待阻塞:运行的线程执行wait方法,jvm会把当前线程放入到等待队列
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么 jvm会把当前的线程放入到锁池中
- 其他阻塞:运行的线程执行Thread.sleep或者t.join方法,或者发出了I/O请求时,JVM会把 当前线程设置为阻塞状态,当sleep结束、join线程终止、io处理完毕则线程恢复
WAITING(等待)
等待状态
TIMED_WAITING(超时等待)
超时等待状态,超时以后自动返回
TERMINATED(终止)
终止状态,表示当前线程执行完毕
以上就是线程的6种状态以及在调用指定方法时线程对应的状态,总结一下可以用一张图来表示
用代码来理解线程的状态
接下来通过一段代码来演示一下线程的状态
public class ThreadStatus {
public static void main(String[] args) {
//运行状态
new Thread(()->{
while (true){
}
},"RUNNABLE(运行)").start();
//超时等待
new Thread(()->{
while(true){
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"TIME_WAITING(超时等待)").start();
//等待状态
//WAITING,线程在ThreadStatus类锁上通过wait进行等待
new Thread(()->{
while(true){
synchronized (ThreadStatus.class){
try {
ThreadStatus.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"WAITING(等待)").start();
//线程在ThreadStatus加锁后,不会释放锁
new Thread(new BlockedDemo(),"BlockA").start();
new Thread(new BlockedDemo(),"BlockB").start();
}
static class BlockedDemo extends Thread{
@Override
public void run(){
synchronized (BlockedDemo.class){
while(true){
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
通过命令 jps找到 ThreadStatus 的PID 执行 jstack PID 打印堆栈信息可以看到
E:\demo>jps
2176 ThreadStatus
4708 Launcher
7700 ComprehensiveLaboratoryApplication
2572
7388 Launcher
9548 Jps
E:\demo>jstack -l 2176
"WAITING(等待)" #14 prio=5 os_prio=0 tid=0x000002266775f000 nid=0x2f24 in Object.wait() [0x000000323c8ff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076bcfe678> (a java.lang.Class for com.zf.osce.infrastructure.implTese.ThreadStatus)
at java.lang.Object.wait(Object.java:502)
at com.zf.osce.infrastructure.implTese.ThreadStatus.lambda$main$2(ThreadStatus.java:29)
- locked <0x000000076bcfe678> (a java.lang.Class for com.zf.osce.infrastructure.implTese.ThreadStatus)
at com.zf.osce.infrastructure.implTese.ThreadStatus$$Lambda$3/1212899836.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"TIME_WAITING(超时等待)" #13 prio=5 os_prio=0 tid=0x0000022667574800 nid=0x34bc waiting on condition [0x000000323c7fe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.zf.osce.infrastructure.implTese.ThreadStatus.lambda$main$1(ThreadStatus.java:17)
at com.zf.osce.infrastructure.implTese.ThreadStatus$$Lambda$2/1973336893.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"RUNNABLE(运行)" #12 prio=5 os_prio=0 tid=0x0000022667573800 nid=0x2540 runnable [0x000000323c6fe000]
java.lang.Thread.State: RUNNABLE
at com.zf.osce.infrastructure.implTese.ThreadStatus.lambda$main$0(ThreadStatus.java:9)
at com.zf.osce.infrastructure.implTese.ThreadStatus$$Lambda$1/1334729950.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
线程的终止
接下来就要聊一聊线程的终止。
首先来屡屡思路
- 过去都是用
//过时的方法
new Thread().stop();
new Thread().suspend();
强制执行,也就说甭管你执行到什么程度,直接给你停掉,和 linux中的kill -9 是类似的。
- 那正常情况下应该是怎么停止呢
(1)不再接收新的请求了
(2)把之前接收的请求都处理完
(3)最后停止
-
停止的方式
run() 方法执行结束,线程停止
有没有个友好的方式来停止。 就好比老师在课堂上讲课,突然有学生不举手就发言,老师话都没说到一半,就被打断,相当的不友好。那学生是不是应该举手示意老师一下,由老师来自主决定是否中断一下,让学生来进行发言。这就是友好的进行中断,由此引出一个方法
interrupt方法
-
根据刚才提到的例子,如果让我们自己来实现这个学生举手示意打断老师讲课该怎么实现。常规做法是不是
boolean flag = false; //当学生举手示意时改为true flag = true; //老师根据flag状态来判断是否需要暂停下给学生讲解一下 if(flag = true){ ...... }
同理,interrupt( ) 是不是也这么做的,接下来我们用代码来感受一下
public class InterruptedDemo implements Runnable{ public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new InterruptedDemo()); t1.start(); } @Override public void run() { //Thread.currentThread().isInterrupted() 获取当前的中断状态 System.out.println("学生是否举手示意"+Thread.currentThread().isInterrupted()); } }
我们得到当前的中断状态默认为false,此时没有进行任何的中断
我们假设 线程t1 为老师,main方法相当于学生,学生要举手示意是不是就相当于向老师发送中断请求,由老师来决定是不是回答学生的问题,代码行如下的改进
public class InterruptedDemo implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new InterruptedDemo());
t1.start();
//为了模拟演示效果 让t1线程充分执行
Thread.sleep(5000);
//调用interrupt() 修改中断状态 相当于学生举手示意
t1.interrupt();
}
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//老师响应中断,老师自己做决定,是继续往下讲,还是直接下课
//catch中的作用 1.唤醒阻塞的线程 2.中断复位
e.printStackTrace();
}
System.out.println("老师"+Thread.currentThread().getName()+ "正在讲课");
}
}
}
运行结果如下
补充一点 interrupt() 源码有明显的注释说明
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag 只是为了设置中断标志
b.interrupt(this);
return;
}
}
interrupt0();
}
关于dump日志
如果线程在执行过程中出现问题,应该怎么去排查 用以下三个点来进行举例
-
cpu占用率高,接口响应很慢
-
cpu占用率不高,接口响应很慢
-
线程出现死锁
注:所有模拟场景都是在linux 环境下
cpu占用率高,接口响应很慢
先上代码
@RestController
@RequestMapping("/thread")
public class LockController {
/**
* 模拟 cpu占用高,响应很慢 场景
*/
@GetMapping("/test1")
public String getResult(){
Thread t1 = new Thread(new ThreadAlways());
t1.start();
try {
//让 t1 线程执行完,营造响应很慢的效果
t1.join();
}catch (InterruptedException e){
e.printStackTrace();
}
return "cpu占用高,响应很慢";
}
class ThreadAlways implements Runnable{
@Override
public void run() {
while (true){
try {
//sleep 1秒 减少点日志打印
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("一直在执行");
}
}
}
}
将以上的代码进行打包 demo-0.0.1-SNAPSHOT.jar
运行 jar包
[root@localhost servers]# nohup java -jar demo-0.0.1-SNAPSHOT.jar > demo.log &
通过 top -c 命令找到占用CPU最高的进程 ,找到它的PID 7647
[root@localhost servers]# top -c
top - 15:44:48 up 7:09, 1 user, load average: 0.60, 0.22, 0.12
Tasks: 212 total, 1 running, 211 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.8 us, 11.5 sy, 0.0 ni, 84.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 1863020 total, 87732 free, 999404 used, 775884 buff/cache
KiB Swap: 2097148 total, 2096884 free, 264 used. 680312 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
7647 root 20 0 4399352 301636 13368 S 120.3 16.2 1:09.23 java -jar demo-0.+
67 root 20 0 0 0 0 S 2.7 0.0 0:00.23 [kswapd0]
5378 root 20 0 0 0 0 S 2.7 0.0 0:00.61 [kworker/u256:0]
239 root 20 0 0 0 0 S 0.3 0.0 0:00.56 [kworker/7:2]
372 root 20 0 0 0 0 S 0.3 0.0 0:26.97 [kworker/1:2]
7793 root 20 0 162132 2432 1668 R 0.3 0.1 0:00.06 top -c
1 root 20 0 194220 7308 4196 S 0.0 0.4 0:03.91 /usr/lib/systemd/+
2 root 20 0 0 0 0 S 0.0 0.0 0:00.06 [kthreadd]
4 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 [kworker/0:0H]
6 root 20 0 0 0 0 S 0.0 0.0 0:00.02 [ksoftirqd/0]
然后再定位到对应的线程, top -H -p 80972 查找到该进程中最消耗CPU的线程 PID 7792
[root@localhost servers]# top -H -p 7674
top - 15:47:52 up 7:12, 1 user, load average: 0.64, 0.44, 0.22
Threads: 38 total, 1 running, 37 sleeping, 0 stopped, 0 zombie
%Cpu(s): 4.4 us, 8.2 sy, 0.0 ni, 87.3 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
KiB Mem : 1863020 total, 82912 free, 910256 used, 869852 buff/cache
KiB Swap: 2097148 total, 2049780 free, 47368 used. 775804 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
7792 root 20 0 4465916 276072 3140 R 99.0 14.8 3:46.80 Thread-4
通过 printf “0x%x\n” 7792 命令,把对应的线程PID转化为16进制
[root@localhost servers]# printf "0x%x\n" 7792
0x1e70
执行命令 jstack 7647| grep -A 20 0x1e70查看线程Dump日志,其中-A 20表示 展示20行, 7647表示进程ID, 0x1e70表示线程ID
[root@localhost servers]# jstack 7647 | grep -A 20 0x1e70
"Thread-4" #32 daemon prio=5 os_prio=0 tid=0x00007f9458140000 nid=0x1e70 runnable [0x00007f942fffe000]
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 <0x00000000e3bbd770> (a java.io.BufferedOutputStream)
at java.io.PrintStream.write(PrintStream.java:482)
- locked <0x00000000e3bbd750> (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 <0x00000000e3bbd840> (a java.io.OutputStreamWriter)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
at java.io.PrintStream.newLine(PrintStream.java:546)
- eliminated <0x00000000e3bbd750> (a java.io.PrintStream)
at java.io.PrintStream.println(PrintStream.java:807)
- locked <0x00000000e3bbd750> (a java.io.PrintStream)
at com.example.demo.controller.LockController$ThreadAlways.run(LockController.java:20)
at java.lang.Thread.run(Thread.java:748)
在dump日志中找到你看着最眼熟的,由此我们知道了LockController 中的ThreadAlways.run方法一直在执行
CPU占用不高,接口相应很慢
依旧是先上代码
/**
* 模拟 cpu占用不高,响应很慢 场景
*/
@GetMapping("/test2")
public String test2(){
Thread t1 = new ThreadA();
Thread t2 = new ThreadB();
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
try {
//调用join是为了体现出响应慢的效果,因为是异步执行,不加会直接return结果
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
return "cpu占用不高,响应很慢";
}
public class ThreadA extends Thread {
@Override
public void run() {
System.out.println("================A===================");
synchronized (ThreadA.class) {
System.out.println("我要开始执行任务A。。。。" + Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ThreadB.class) {
}
System.out.println("我在执行任务结束了A。。。。" + Thread.currentThread().getName());
}
}
}
public class ThreadB extends Thread{
@Override
public void run() {
System.out.println("================B===================");
synchronized (ThreadB.class) {
System.out.println("我要开始执行任务B。。。。" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ThreadA.class) {
}
System.out.println("我在执行任务结束了B。。。。" + Thread.currentThread().getName());
}
}
}
访问test2接口会一直等待,因为两个线程在互相抢占锁造成死锁。检查办法 是 jps 找到这个接口所在服务的pid 通过jstack命令我们可以看到如下结果
Found one Java-level deadlock:
=============================
"线程2":
waiting to lock monitor 0x00007feacc0062c8 (object 0x00000000f7b9ddf0, a java.lang.Class),
which is held by "线程1"
"线程1":
waiting to lock monitor 0x00007fea60002318 (object 0x00000000f7ba18f8, a java.lang.Class),
which is held by "线程2"
Java stack information for the threads listed above:
===================================================
"线程2":
at com.example.demo.application.ThreadB.run(ThreadB.java:15)
- waiting to lock <0x00000000f7b9ddf0> (a java.lang.Class for com.example.demo.application.ThreadA)
- locked <0x00000000f7ba18f8> (a java.lang.Class for com.example.demo.application.ThreadB)
"线程1":
at com.example.demo.application.ThreadA.run(ThreadA.java:15)
- waiting to lock <0x00000000f7ba18f8> (a java.lang.Class for com.example.demo.application.ThreadB)
- locked <0x00000000f7b9ddf0> (a java.lang.Class for com.example.demo.application.ThreadA)
Found 1 deadlock.