了解并发和并发的价值
- 提高系统性能:多线程并发执行可以让多个任务同时运行,提高系统吞吐量和响应速度。对于大规模数据处理、网络通信、数据库操作等场景,通过并发可以提高系统的处理能力。
- 提高资源利用率:并发可以充分利用多核处理器的性能,同时处理多个任务,避免多核处理器的闲置。通过合理的任务调度和资源管理,可以最大程度地利用系统资源。
- 增强用户体验:并发可以提高系统的响应速度和并发度,减少用户等待时间。对于Web应用程序、多媒体播放器等需要实时响应的应用,通过并发可以提升用户体验。
- 实现复杂逻辑:并发可以将复杂的问题划分为多个独立的任务进行处理,简化任务的逻辑和代码编写。通过分解问题,可以提高程序的模块化程度,降低复杂度,便于开发和维护。
了解线程
线程是计算机中的基本执行单元在单个程序中,多个线程可以同时执行,并且每个线程都具有自己的代码、数据和执行路径。线程可以在同一个进程内共享资源和内存,因此它们相互之间可以更快地通信和共享数据。
线程的使用可以提高计算机系统的效率和性能。在应用程序中使用多个线程可以并行执行多个任务,并且可以实现同时处理多个用户请求。线程的并发执行可以提高计算机系统的吞吐量,减少响应时间,并提高用户体验。
在操作系统中,线程是由调度器进行管理和调度的。调度器根据各个线程的优先级和调度策略来决定哪个线程可以执行,并且在合适的时候切换线程的上下文,以实现多个线程的并发执行。
线程还可以通过同步机制来实现对共享资源的安全访问。通过使用锁、信号量和条件变量等同步机制,可以避免多个线程同时访问共享资源引发的数据竞争和不一致性。
线程的使用也有一些潜在的问题,例如线程安全问题、死锁、活锁和竞争条件等。因此,在使用线程时需要注意这些问题,并采取合适的措施来解决和避免这些问题。
Java中的线程的实现
- 继承Thread类:创建一个新类,继承自Thread类,并重写run()方法。然后创建该类的对象,并调用start()方法启动线程。
例如:
public class MyThread extends Thread {
public void run() {
// 线程执行的代码
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- 实现Runnable接口:创建一个新类,实现Runnable接口,并实现其中的run()方法。然后创建Thread对象,将实现了Runnable接口的类的对象作为参数传递给Thread的构造方法,并调用start()方法启动线程。
例如:
public class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
- 使用Executor框架:通过Executor框架提供的方法来创建和管理线程。可以使用Executors类来创建不同类型的ExecutorService,然后使用submit()方法提交任务。
例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(new Runnable() {
public void run() {
// 线程执行的代码
}
});
executor.shutdown();
}
}
以上是Java中线程的三种常见实现方式,开发者根据实际需求选择适合的方式。还可以通过Callable和Future来实现带返回值的线程,以及通过Lock和Condition来实现线程之间的通信。
多线程的基本原理
接下来我们了解一下多线程的基本原理,整体的原理如下。线程的start方法,实际上底层做了很多事情,具体的实现简图如下,画得不一定工整,但是能够表达大概意思就行。
OS调度算法有很多,比如先来先服务调度算法(FIFO)、最短优先(就是对短作业的优先调度)、时间片轮转调度等。
线程的启动和停止
首先,我们先来了解线程的运行状态,
package yxy;
public class InterruptExample implements Runnable{
volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new InterruptExample());
t1.start();
Thread.sleep(2000);
//把中断权交出去,核心就是下面这句
t1.interrupt();//发送一个中断信号,中断暴击为true
// 让当前线程中断状态的复位
// Thread.interrupted();
}
@Override
public void run() {
//如果让线程友好结束,只有当前run方法中的程勋知道
//Thread.currentThread().isInterrupted();
while (!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//处理自己决定是中断,还是不中断,核心在下面这句,我来接受
Thread.currentThread().interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--");
}
}
}
运行上述示例,打开终端命令,输入"jps"(显示当前所有Java进程pid);
根据获取到的pid, 通过jstack pid ,可以打印指定Java进程ID的堆栈信息
通过堆栈信息,可以看到线程的运行状态线程的状态
通过上面这段代码可以看到,线程在运行过程中,会存在几种不同的状态,一般来说,在Java中,线程
的状态一共是6种状态(因为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等,这些方法可以终止一个线程或者挂起一个线程,但是这些方法都不建议大家使用。原因比较简单,
举个例子,假设一个线程中,有多个任务在执行,此时,如果调用stop方法去强行中断,那么这个时候相当于是发送一个指令告诉操作系统把这个线程结束掉,但是操作系统的这个结束动作完成不代表线程中的任务执行完成,很可能出现线程的任务执行了一般被强制中断,最终导致数据产生问题。这种行为类似于在linux系统中执行 kill -9类似,它是一种不安全的操作。
那么除了这种方法之外,还有什么方式可以实现线程的终止呢?要了解这个问题,我们首先需要知道,一个线程什么情况下算是终止了。
一个线程在什么情况下是执行结束了
我们分析一下下面这段代码,通过start()启动一个线程之后,本质上就是执行这个线程的run方法。那么如果这个线程在run方法执行完之前,一直处于运行状态,直到run方法中的指令执行完毕,那么这个线程就会被销毁。
package yxy;
public class MyThread extends Thread {
public void run() {
System.out.println("My Thread");
}
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
myThread1.start();
}
}
在正常情况下,这个线程是不需要人为干预去结束的。如果要强制结束,只能走stop这个方法。
那在哪些情况下,线程的中断需要外部干预呢?
- 线程中存在无限循环执行,比如while(true)循环
- 线程中存在一些阻塞的操作,比如sleep、wait、join等。
存在循环的线程
假设存在如下场景,在run方法中,存在一个while循环,因为这个循环的存在使得这个run方法一直无法运行结束,这种情况下,如何终止呢?
package yxy;
public class MyThread extends Thread {
public void run() {
while (true) {
System.out.println("My Thread");
}
}
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
myThread1.start();
}
}
按照我们开发的思维来说,首先要解决的就是,while(true)这个循环,必须要有一个结束条件,其次是要在其他地方能够修改这个结束条件让该线程感知到变化。假设我们把while(true)改成while(flag),这个flag可以作为共享变量被外部修改,修改之后使得循环条件无法被满足,从而退出循环并且结束线程。
这段逻辑其实非常简单,其实就是给了线程一个退出的条件,如果没有这个条件,那么线程将会一直运行。
实际上,在Java提供了一个 interrupt 方法,这个方法就是实现线程中断操作的,它的作用和上面讲的这个案例的作用一样。
interrupt方法
当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。
线程通过检查资深是否被中断来进行相应,可以通过isInterrupted()来判断是否被中断。
package yxy;
import java.util.concurrent.TimeUnit;
public class InterruptDemo {
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("NUM"+i);
},"interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();//加和不加的区别,不加阻塞,加可顺利结束
}
}
这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅,运行结果如下图:
处于阻塞状态下的线程中断
另外一种情况,就是当线程处于阻塞状态下时,我想要中断这个线程,那怎么做呢?
public class InterruptDemo {
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()){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//i++;
}
System.out.println("NUM"+i);
},"interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();//加和不加的区别,不加阻塞,加可顺利结束
}
}
从这个例子中反馈出一个问题,我们平时在线程中使用的sleep、wait、join等操作,它都会抛出一个InterruptedException异常,为什么会抛出异常,是因为它在阻塞期间,必须要能够响应被其他线程发起中断请求之后的一个响应,而这个响应是通过InterruptedException来体现的。(理解为InterruptedException发生了中断)运行结果如下图:
但是这里需要注意的是,在这个异常中如果不做任何处理的话,我们是无法去中断线程的,因为当前的异常只是响应了外部对于这个线程的中断命令,同时,线程的中断状态也会复位,如果需要中断,则还需要在catch中添加下面的代码
package yxy;
import java.util.concurrent.TimeUnit;
public class InterruptDemo {
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()){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
//核心在这
Thread.currentThread().interrupt();再次中断
}
//i++;
}
System.out.println("NUM"+i);
},"interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();//加和不加的区别,不加阻塞,加可顺利结束
}
}
所以,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如
1. 直接捕获异常不做任何处理
2. 将异常往外抛出
3. 停止当前线程,并打印异常信息
Thread Dump日志分析
接下来给大家再讲点在工作中比较实用的一个内容。就是我们在使用线程的时候,如果出现问题,怎么排查? 比如说
CPU占用率很高,响应很慢
CPU占用率不高,但响应很慢
线程出现死锁的情况
演示代码
为了更好的体现效果,我们通过“thread-demo”这个项目进行演示。
CPU占用率不高,但响应很慢
通过 curl http://127.0.0.1:8088/dead 演示死锁的场景
查看死锁问题的操作步骤如下:
- 通过 jps 命令,查看java进程的pid
- 通过`jstack 查看线程日志
如果存在死锁情况,Thread Dump日志里面肯定会给出Found one Java-level deadlock:信息。只要找到这个信息就可以立马定位到问题并且去解决。
从上述内容可以看出,是WhileThread.run方法中,执行的逻辑导致CPU占用过高。
CPU占用率很高,响应很慢
有的时候我们会发现CPU占用率很高,系统日志也看不出问题,那么这种情况下,我们需要去看一下运行中的线程有没有异常。
执行 curl http://127.0.0.1:8088/loop 这个方法,会出现一个线程死循环的情况。
- 通过 top -c 动态显示进程及占用资源的排行榜,从而找到占用CPU最高的进程PID,得到的PID=80972
- 然后再定位到对应的线程, top -H -p 80972 查找到该进程中最消耗CPU的线程,得到 PID=81122
- 通过 printf “0x%x\n” 81122 命令,把对应的线程PID转化为16进制
- 截止执行这个命令 jstack 80972 | grep -A 20 0x13ce2 查看线程Dump日志,其中-A 20表示展示20行, 80972表示进程ID, 0x13ce2表示线程ID
从上述内容可以看出,是WhileThread.run方法中,执行的逻辑导致CPU占用过高。