- 哈喽,大家好,这篇文章主要讲解Java并发编程,线程基础的知识点,讲解知识点的同时也会穿插面试题的讲解.
主要讲解以下内容
- 进程,线程基础常见的一些区别比较
- 如何使用和查看线程
- 理解线程如何运行以及线程上下文切换的知识
- 线程方法
- 线程状态
希望能给大家带来帮助.加油~~~
目录
进程,线程基础常见的一些区别比较
进程与线程的区别
并发与并行的区别
Java中守护线程和用户线程的区别?
编辑
说下同步、异步、阻塞和非阻塞
JDK19中的虚拟线程了解么? /了解协程么?
线程的使用与查看
线程创建的方式有哪些?/Java中线程的实现方式?
继承Thread类
普通类继承Thread类
匿名内部类
继承Thread类的缺点
实现Runnable接口
普通类实现Runnable接口
匿名Runnable实现类
使用lambda表达式
实现Runnable接口的缺点
实现Callable接口创建线程
普通类实现Callable接口
匿名callable实现类
实现Callable接口来创建线程分析
使用线程池创建线程
Runnable与Callable的区别?
Thread 与 Runnable 的关系
线程类的构造方法,静态块是被哪个线程调用的?
cpu占用过高的分析思路
如何查看进程线程的方法
线程上下文切换/如何理解线程的运行
栈与栈帧
多线程运行的原理
多线程运行原理
什么是线程上下文切换
发生上下文切换的原因
线程数越多越好么 ?
线程方法
start与run
start与run的区别
为什么调用start()方法时会执行run()方法,而不直接执行run()方法?
为什么start方法不能重复调用?而run方法却可以?
可以直接调用 Thread 类的 run 方法吗?
sleep,yield,wait
sleep与yield的区别
sleep与wait的区别
为什么 wait() 方法不定义在 Thread 中?
为什么 sleep() 方法定义在 Thread 中?
线程的优先级
Thread.sleep(0)的作用是什么?
join方法
interrupt方法
如何停止一个正在运行的线程? /如何优雅的关闭线程
错误停止线程的方式
interrupt, isInterrupted, interrupted的区别是什么
notify和notifyAll有什么区别
线程的状态
线程的5种状态
线程的六种状态
线程状态之间的变化
线程死亡的几种情况
线程阻塞的几种情况
进程,线程基础常见的一些区别比较
进程与线程的区别
什么是进程 ?
比如我们打开一个程序,就相当于开启一个进程,我们知道程序是由指令和数据构成的,这些指令要运行,数据要读写就必须将指令加载到CPU,数据加载到内存中,指令运行的过程中需要用到磁盘,网络等设备,进程就是用来加载指令,管理内存,管理IO的.
总结一句话 : 当一个程序被运行,从磁盘加载这个程序的代码到内存,这时就开启了一个进程.
什么是线程 ?
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行.
一个进程包含了多个线程,Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。
总结一下进程与线程的区别:
- 进程是资源分配的基本单位,线程是程序执行/系统调度的基本单位;
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 进程拥有独立的内存空间和资源,而线程则共享进程的内存和资源
- 进程之间的通信比较复杂,而线程通信相对简单,他们共享进程中的资源
- 线程更加轻量,线程的上下文切换成本一般要比进程的上下文切换低
并发与并行的区别
对于单核CPU : 线程实际上还是串行执行的.
由于操作系统有一个组件叫做任务调度器,他会将CPU的时间片分给不同的程序使用,只是由于CPU在线程间的切换速度非常快,所以我们人总感觉是同时执行的.
即 : 微观串行,宏观并行.
我们把 对于线程轮流使用CPU的这种做法叫做并发.
在多核CPU下,才能真正的可以做到并行执行,也就是多个CPU核心都可以调度运行线程.
总结一下 :
- 并发是同一时间应对多件事情的能力,比如多个线程轮流使用一个或者多个CPU
- 并行是同一时间动手做多件事情的能力,比如4核CPU同时执行4个线程
现在大多数都是多核CPU,在多核CPU下 , 没有绝对的并发和绝对的并行,大多数情况下都是既有并发又有并行.
Java中守护线程和用户线程的区别?
用户线程 : 默认情况下我们创建的线程或线程池都是用户线程,所以用户线程也被称之为普通线程。
守护线程 : 守护线程是为用户线程服务的,一般是指后台线程.
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。当程序中的用户线程全部执行结束之后,守护线程也会结束.
- 垃圾回收器线程: 垃圾回收就是会将堆中的没有被引用的对象回收掉,当程序停止的时候,垃圾回收线程也会停止
- 守护线程的设置 setDaemon(true) 必须要放在线程的 start() 之前,否则程序会报错。也就是说在运行线程之前,一定要先确定线程的类型,并且线程运行之后是不允许修改线程的类型的。
@Slf4j(topic ="c.TestDemo1")
public class TestDemo1 {
public static void main(String[] args) throws InterruptedException {
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
try {
sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
}
}
说下同步、异步、阻塞和非阻塞
JDK19中的虚拟线程了解么? /了解协程么?
线程的使用与查看
线程创建的方式有哪些?/Java中线程的实现方式?
线程的创建/实现方式有四种
- 继承Thread类
- 普通类继承Thread类
- 匿名内部类
- 实现Runnable接口
- 普通类实现Runnable接口
- 匿名Runnable实现类
- 使用lambda表达式
- 实现Callable接口
- 普通类实现Callable接口
- 匿名callable实现类
- 使用线程池创建线程
继承Thread类
普通类继承Thread类
//使用普通类继承Thread
class MyThread extends Thread{
@Override
public void run() {
System.out.println("我使用的是普通类来继承Thread类");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
}
}
匿名内部类
public class ThreadDemo2 {
//使用匿名内部类的方式创建线程
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("我是run方法");
}
};
//创建线程
t.start();
}
}
继承Thread类的缺点
- 使用继承Thread类的方式创建新线程,最大的局限就是不支持多继承,因为Java语言的特点就是单继承.继承了Thread类就不能继承其他类了
实现Runnable接口
普通类实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("我使用普通类实现Runnable接口来创建线程");
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t = new Thread(myRunnable);
t.start();
}
}
匿名Runnable实现类
public static void main(String[] args) {
//使用匿名的Runnable实现类
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是run方法");
}
});
//启动线程
t.start();
}
使用lambda表达式
public static void main(String[] args) {
//使用Lambda表达式
Thread t = new Thread(()->{
System.out.println("我是Lambda创建的线程");
});
//启动线程
t.start();
}
实现Runnable接口的缺点
实现Runnable接口和继承Thread类的缺点都是不能获取线程执行的结果.但是如果是在不要求得到线程执行的返回结果的情况下就使用Lambda表达式最简洁
FutureTask也要需要与callable进行配合使用,futureTask参数要加上callable
实现Callable接口创建线程
普通类实现Callable接口
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i =0;i<100;++i){
sum += i;
}
return sum;
}
}
public class ThreadDemo6 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int res = futureTask.get();
System.out.println(res);
}
}
匿名callable实现类
public static void main(String[] args) throws ExecutionException, InterruptedException {
//使用匿名callable实现类
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i =0;i<100;++i){
sum += i;
}
return sum;
}
});
Thread t = new Thread(futureTask);
t.start();
int res = futureTask.get();
System.out.println(res);
}
实现Callable接口来创建线程分析
callable接口要作为参数放到FutureTask对象中.
根据源码分析 FutureTask 是间接实现了 Runnable接口,所以FutureTask也可以作为一个任务对象,他还有一个future接口,这个接口可以用于得到线程执行的结果,通过futureTask.get()就可以获取到线程执行结果交给其他线程来使用. 而对于Runnable是不能得到线程执行的结果
- 首先要创建好Callable实例(或者使用匿名内部类的方式),将Callable实例传入FutureTask的构造方法参数中
- 然后创建线程对象,将FutureTask对象传入到线程对象构造参数中.
- 当启动线程后,就会执行callable的call方法,然后将结果返回给FutureTask对象保存起来
- 然后线程执行完毕之后,可以通过futureTask.get()得到线程执行的结果
如果需要线程的执行结果,那就可以使用实现Callable接口的方式
使用线程池创建线程
//使用线程池来创建线程
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("使用线程池来创建线程!!!");
}
});
Runnable与Callable的区别?
首先实现Runnable接口和实现Callable接口都可以创建线程
两者的区别 :
- Callable的任务执行后有返回值,而Runnable的任务是没有返回值的. , 运行Callable任务可以拿到一个Future对象, 可以得到异步计算的结果. 对于Runnable不能获取线程的执行结果
- Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()
- Callable接口的call() 方法允许抛出异常;而Runnable接口的run方法的异常只能内部消化,不能继续上抛.
Thread 与 Runnable 的关系
Thread t1 = new Thread(() ->{
System.out.println("xxxx");
}
);
Runnable runnable = ()->{
System.out.println("xxx");
};
Thread t2 = new Thread(runnable);
- 通过源码我们可以发现,他们最终都是调用的是run方法,有Runnable就调用Runnable的run方法
- 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活 ,组合优于继承
线程类的构造方法,静态块是被哪个线程调用的?
线程类的构造方法和静态块是由主线程调用的。
在Java程序中,当我们创建一个线程对象时,实际上是在主线程中创建了一个新的线程对象。这个新线程的构造方法和静态块都是在主线程中被调用执行的。
具体来说,在主线程中创建线程对象时,会调用线程类的构造方法来初始化线程对象,并执行构造方法中的逻辑。如果线程类中有静态块,主线程在加载这个类的时候会执行静态块中的代码,完成静态资源的初始化工作。
一旦创建完线程对象,我们可以通过调用
start()
方法启动线程,此时主线程会调用新线程的run()
方法,新线程开始独立执行,并与主线程并发运行。
cpu占用过高的分析思路
- top -H -p 进程id -->查看进程中的线程信息 ,先使用top命令找出CPU占比最高的 到底是哪一个进程CPU过高
- 查看线程信息 : ps H -eo pid,tid,%cpu | grep 进程pid 进一步知道进程中的哪一个线程CPU过高
- 可以使用jstack 进程id->pid 得到该进程的所有线程栈的信息 ->线程的具体信息
- 因为上一步之前得到的tid是10进制的,所以需要把cpu过高的那个线程的tid转换成16进制,在线程栈信息中找该16进制的tid
如何查看进程线程的方法
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
- ps -ef | grep 进程名称
-
查看该进程及其所有子线程:ps -efL|grep PID
- kill 杀死进程
-
用于查看系统中所有线程,比如:top -H
- top -H -p <PID> 查看某个进程(PID)的所有线程
- jps 命令查看所有 Java 进程,以及进程pid
- jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
线程上下文切换/如何理解线程的运行
先理解栈帧的概念,我们一步一步了解线程的运行原理
栈与栈帧
JVM是由堆,栈,方法区所组成.我们的栈内存其实就是给我们线程来使用的.每一个线程启动后虚拟机就会为其分配一块栈内存
每一个栈是由多个栈帧(Frame)组成的,栈帧就是一次方法调用所占用的内存
每一个线程只能有一个活动栈帧,就是对应着当前正在执行的方法
每一个栈帧对应一个方法的调用
我们启动main方法的时候==>对应着就启动一个主线程,主线程一启动,虚拟机就会为主线程分配一块栈内存(由许多栈帧组成),调用main方法就会为main方法产生一个栈帧
在调用方法就会对其方法在产生一个栈帧(压栈),方法执行完之后,栈内存就会被释放,同时调用的时候记录返回地址,执行完就回到返回地址处,然后继续执行
我们可以发现每调用以此方法都会有栈帧入栈,当方法运行结束之后,栈帧会弹出 , 同时调用的时候记录返回地址,执行完就回到返回地址处,然后继续执行
多线程运行的原理
多线程运行原理
public class TestThreadDemo1 {
public static void main(String[] args) {
Thread t = new Thread("t1"){
@Override
public void run() {
method(10);//t1线程调用的方法
}
};
t.start();
method(10);//主线程调用的方法
}
public static void method(int x){
int y = x + 1;
Object m = method2();
System.out.println(m);
}
public static Object method2(){
Object n = new Object();
return n;
}
}
总结一下就是 :
多线程运行的时候,每一个线程都会有自己的栈帧,线程的栈内存相互独立(每个线程拥有自己的独立的栈内存,栈内存里面有多个栈帧),互不干扰,当系统内没有任何线程执行的时候,整个程序就结束了
什么是线程上下文切换
在多线程运行的时候,CPU通过时间片分配算法来循环执行任务 , 任务调度器会给每一个线程分配CPU时间片,当一个线程执行完一个时间片的任务之后就会切换到下一个任务,就会把CPU的使用权交给其他线程,在切换前我们也要保存上一个任务的状态,以便下一次切换回这个任务的时候,可以加载这个任务的状态,继续向下运行,该线程从使用CPU->不使用CPU 就是一次线程的上下文切换(任务从保存到再加载的过程就是一次上下文切换)
发生上下文切换的原因
被动发生上下文切换
- 时间片用完
也就是当任务调度器将CPU时间片分配给每一个线程运行,这个时间片总有用完的时候,时间片用完的时候,CPU的使用权就会交给其他线程,这个时候对于当前线程就是发生一次上下文切换.
- 垃圾回收
垃圾回收会暂停当前工作的线程(就会出现上下文切换),就会让垃圾回收的线程开始回收垃圾
- 更高优先级的线程要运行
CPU的使用权就要交给优先级更高的线程来使用,当前线就会暂停(就是一次线程的上下文切换),当这个优先级更高的线程运行完了之后,由任务调度器会重新为当前线程分配CPU时间片
主动发生上下文切换
- 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法
当完成一次线程的上下文切换的时候,操作系统就要保存当前线程的状态(包括需要使用程序计数器记住下一条JVM的指令执行地址,还有一些状态信息,虚拟机栈的栈帧信息,如局部变量,操作数栈,返回地址....),并且恢复另一个线程的状态
为啥要保存当前线程的状态呢 ?
原因是当我们下次在运行的时候,CPU就知道要从哪开始运行了.
使用程序计数器就可以保存当前代码执行到哪里了(也就 是记住下一跳JVM指令的地址).
频繁的上下文切换会影响性能.
并不是线程数越多越好,当CPU核心数 < 线程数,线程的上下文切换的成本就会提高.频繁的上下文切换就会影响性能.
线程数越多越好么 ?
不是的,当线程数超过CPU核心数的时候,CPU就要在这么多线程轮流切换, 线程的上下文切换成本会提高,频繁的上下文切换影响程序的性能.
线程方法
start与run
start与run的区别
- 调用start方法才是真正的启动一个新的线程执行,调用run方法只是执行了一个普通方法不没有真正的创建线程让线程执行,run方法只是封装了要被线程执行的任务
- 调用start方法不会立即执行任务 , 调用run就可以立即执行.
因为调用start方法的时候不是立即执行线程任务,而是从新建状态变为就绪状态,就绪状态就是已经获取到了除CPU资源以外的其他资源,等待任务调度器分配给当前线程CPU时间片,才会真正的执行任务. 所以调用start方法不会立即执行任务. 但直接调用run,run相当于是普通方法,可以立即执行
- 重复调用start方法就会抛出异常,对于run方法为普通方法可以重复调用
为什么调用start()方法时会执行run()方法,而不直接执行run()方法?
首先我们使用多线程的原因就是提高速度,创建多个线程来执行任务.
- 调用start方法才会启动一个新的线程,并在新线程中执行run方法的代码.因为start方法会做一些准备工作,包括线程分配资源,初始化线程上下文,然后等到获取到CPU时间片, 调用操作系统相关API创建新的线程
- 直接调用run方法,会在当前线程中执行run方法代码,并不会启动一个新的线程去执行任务
为什么start方法不能重复调用?而run方法却可以?
当我们重复调用start方法的时候就会出现异常...
可以看源码进行分析 :
0->表示NEW状态.
调用start方法的时候,首先会先判断线程状态(threadStatus)是不是等于0(是不是新建状态),如果不是新建状态了,那就会抛出异常.原因是当调用第一次start方法的时候,线程状态从NEW变为了Runnable状态,由于线程状态是不可逆的(也就是说不能在从Runnable->NEW了),所以JVM在判断当前线程状态已经不是NEW状态了,他就会抛出异常.
即 : 重复调用 start()会抛出异常
可以直接调用 Thread 类的 run 方法吗?
可以通过Thread类对象直接调用run方法,run方法就是一个普通方法,用于封装线程执行的任务, 如果直接调用run方法会直接在当前线程(一般为主线程)执行,不会新启动一个线程去执行任务.
sleep,yield,wait
sleep与yield的区别
- sleep()方法使线程进入阻塞状态,暂停执行指定的时间,在时间结束后重新进入就绪状态. yield()方法使线程放弃当前获得的CPU时间片,重新回到就绪状态
- sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会,yield()方法只会给相同或更高优先级的线程以运行的机会;
- 线程执行sleep()方法后进入阻塞(blocked)状态,而执行yield()方法后进入就绪(ready)状态;
- sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
sleep与wait的区别
共同点:两者都可以暂停线程的执行.
两者的区别 :
- wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
- sleep 不需要强制与 synchronized 配合使用, 但是 wait 必须和 synchronized 一起使用.调用wait方法前必须先获取wait对象的锁
- 调用sleep方法后不会释放对象锁, 但调用wait方法之后就会释放对象锁,允许其他线程获取锁
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法.或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。sleep()方法执行完成后,线程会自动苏醒 并且他们都可以被打断唤醒
- sleep()方法调用后, 线程进入TIMED_WAITING状态, wait()方法调用后未被唤醒前是WATING状态。 wait(n),sleep(n)两者共同点就是 : 线程的状态都为TIMEDWAITING
- sleep是Thread类的静态方法,而wait是所有对象都有的方法,是Object的方法
为什么 wait() 方法不定义在 Thread 中?
因为调用wait方法前必须获取对象锁,让获取对象锁的线程等待,然后就会自动释放当前线程占有的对象锁.
每一个对象都拥有对象锁,要想让获取到对象锁的线程进入WAITING状态,操作的是对应的对象,而不是线程.
为什么 sleep()
方法定义在 Thread
中?
sleep方法是让当前线程暂停执行,没有涉及到对象类,就不需要获取对象锁.
线程的优先级
Thread.sleep(0)的作用是什么?
Thread的sleep会让线程暂时释放CPU资源,然后进入TIMED_WAITING状态,等待指定时间之后在尝试获取CPU时间片.
Thread.sleep(0),就是让当前线程释放一下CPU时间片,然后重新开始争抢.
可以用来做线程调度,比如某个线程长时间占用CPU资源,这个时候sleep(0)让线程主动释放CPU时间片,让其他线程可以进行一次公平的竞争
join方法
join()
方法的作用是什么?
join方法就是用于一个线程等待另外一个线程执行完毕,然后自己可以利用那个线程执行完的结果做一些其他事情.
join()
方法与sleep()
方法有什么区别?
- join方法用于一个线程等待另外一个线程执行完毕, sleep方法用于让当前线程暂停一段时间
- join方法必须在线程对象上调用,而
sleep()
是Thread类的静态方法,通过Thread类调用 - join方法进入WAITING状态, sleep方法进入TIMED_WAITING状态
- 多个线程之间如何正确使用
join()
方法来协同工作?/如何使用join()
方法来保证某些线程在其它线程执行完毕后再执行?
多个线程之间使用join方法,主要是确保线程之前的顺序执行.
我们需要创建多个协作的线程,哪个线程需要等待另外一个线程结束,就在该线程内部调用另外一个线程的join方法
比如 main线程等待t1,t2结束,那么就在主线程调用t1.join,t2.join
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("t1");
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println("t2");
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("main");
}
如果t1要想等待t2结束,那么就在t1线程中调用t2.join方法
public static void main(String[] args) throws InterruptedException {
Thread t2 = new Thread(() -> {
System.out.println("t2");
}, "t2");
Thread t1 = new Thread(() -> {
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1");
}, "t1");
t1.start();
t2.start();
t1.join();
System.out.println("main");
}
- 如果一个线程已经完成了它的任务,再次调用它的
join()
方法会发生什么?
如果一个线程已经完成了它的任务,再次调用它的 join()
方法将不会发生任何变化,因为已经没有需要等待的内容了。
interrupt方法
如何停止一个正在运行的线程? /如何优雅的关闭线程
有三种方式可以用来停止线程
- 使用标志位,来终止其他线程的run方法
@Slf4j(topic = "c.TestInterrupt")
public class InterruptTest1 {
//1.使用自带标记位
private static volatile boolean flg = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (!flg) {
log.debug("test...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"t1").start();
Thread.sleep(2000);
flg = true;
}
}
- 使用stop方法强行终止
-
@Slf4j(topic = "c.InterruptTest2") public class InterruptTest2 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(true) { log.debug("test..."); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, "t1"); t1.start(); Thread.sleep(2000); t1.stop(); } }
- 使用interrupt方法打断该线程
- 打断正在阻塞的线程.(比如调用了sleep,wait,join方法,都会进入阻塞状态这些方法都要抛出InterrupedException)
public static void main(String[] args) throws InterruptedException {
//1.打断阻塞的线程
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(600);
t1.interrupt();
log.debug(t1.isInterrupted()+"");
}
被阻塞的线程打断标记会被设置为false,将来打断标记可以用来判断这个线程是继续运行还是就此终止.
- 打断正常运行的线程.
@Slf4j(topic = "c.InterruptTest4")
public class InterruptTest4 {
public static void main(String[] args) throws InterruptedException {
//1.打断正在运行的线程
Thread t1 = new Thread(() -> {
while(true) {
log.debug("sleep");
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
//由被打断线程自己去决定,是继续运行还是停止
break;
}
}
}, "t1");
t1.start();
t1.interrupt();
}
}
打断正在运行的线程也是使用t.interrupt()方法来进行打断, 会把打断标记设置为true,被打断线程可以根据打断标记来判断,接下来是继续运行还是就此停止.
错误停止线程的方式
- 使用stop方法强行终止
@Slf4j(topic = "c.InterruptTest2")
public class InterruptTest2 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
log.debug("test...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "t1");
t1.start();
Thread.sleep(2000);
t1.stop();
}
}
不建议使用stop方法来终止线程,这样不是优雅的终止,而是强制终止.
interrupt, isInterrupted, interrupted的区别是什么
- void interrupt() :interrupt() 方法只是给当前线程设置了一个打断标记,把打断标记置为true,至于后续是打断还是继续运行,由当前线程自己决定(料理完后事在退出).对于打断阻塞的线程(调用了wait方法,sleep方法,join方法),会认为是异常终止,会把打断标记置为false
- boolean isInterrupted() 方法: 检测当前线程是否被中断。
- boolean interrupted() 方法:检测当前线程是否被中断, 与isInterrupted 不同的是,该方法如果发现当前线程被中断, 则会清除中断标志.
notify和notifyAll有什么区别
- notify和notifyAll都是操作对象的,所以都被定义在Object中,都是Object的方法
- notify 是把在等待队列(WaitSet)中等待的线程,随机唤醒一个
- notifyAll : 是把在等待队列(WaitSet)中等待的线程全部唤醒
还有一点要注意的是,从等待队列中被唤醒并不能立即获取到CPU的使用权,只是表示他们可以竞争锁了,竞争到锁之后才有机会被CPU调度
notifyAll虽然唤醒的是waitset中的所有线程,但是最终只能有一个线程获取/竞争到锁.
线程的状态
线程有5种状态和6种状态两种说法
线程的5种状态是指操作系统层面上的 , 线程的6种状态是指JavaAPI层面上的
线程的5种状态
线程的5种状态是指操作系统层面上的
- 初始状态 : 仅仅是语言层面创建了线程对象,还没有与操作系统相关联.比如new 了一个Thread对象还没有调用start方法
- 可运行状态 : 也可以理解为就绪状态,该线程已经被创建了,与操作系统相关联,可以由CPU调度,但是线程调度器还没有给该线程分配CPU时间片
- 运行状态 : 给该线程分配了CPU时间片,线程处于正在运行的状态
- 这里运行状态和可运行状态是能进行上下文切换的,当CPU时间片用完了,线程就会从运行状态变为可运行状态.,导致线程上下文切换
- 阻塞状态 : 该线程调用了一些阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,就会导致上下文切换,从运行状态变为阻塞状态.线程调度器不会分配给[阻塞状态线程]CPU时间片的,但是会分配给可运行状态的线程. 当读写文件操作完毕,会由操作系统来唤醒阻塞状态的线程, 就会从阻塞状态变为可运行状态
- 终止状态 : 当当前线程的所有代码执行完成之后,表示线程已经执行完毕,生命周期已经结束了,不会再转换为其他状态.
线程的六种状态
线程的6种状态是JavaAPI层面上的.
根据Thread.State 枚举 ,分为6种状态.
- NEW : 仅仅是语言层面创建了线程对象,还没有与操作系统相关联.比如new 了一个Thread对象还没有调用start方法
- RUNNABLE : JavaAPI层面上状态涵盖了操作系统层面的[可运行状态],[运行状态] 和[阻塞状态]
- 阻塞状态在Java里面细分为 BLOKED,WAITING,TIMED_WAITING
- BLOKED : 当t线程调用synchronized(obj) 获取对象锁时竞争失败,会进入BLOCKED状态
- WAITING : 当调用线程的join方法,LockSupport.park()方法或者用synchronized(obj)获取了对象锁后,调用obj.wait()方法时.
- TIMED_WAITING : 当用synchronized获取对象锁后,调用wait(long n)方法时候,当调用join(long n) ,调用sleep(long n),或者调用LockSupport的有时限的方法时
- TERMINATED : 当当前线程的所有代码执行完成之后,表示线程已经执行完毕,生命周期已经结束了,不会再转换为其他状态.
线程状态之间的变化
- NEW --> RUNNABLE
当调用了 t.start()方法的时候,由NEW --> RUNNABLE
- RUNNABLE <--> WAITING
调用wait方法,join方法,LockSupport.park() 线程就从 RUNNABLE --> WAITING
- 对于join方法和park方法 当其他线程调用interrupt方法打断该线程就会 从 WAITING-->RUNNABLE状态
- 对于wait方法
- 调用了 obj.notify() ,obj.notifyAll() ,t.interrupt() 时候
- 竞争锁成功,t线程从 WAITING --> RUNNABLE
- 竞争锁失败,t线程从 WAITING --> BLOCKED
比如两个线程竞争同一把锁,其中主线程使用notifyAll把它们全唤醒,那必然有一个线程竞争锁成功从WAITING --> RUNNABLE,必然有一个线程竞争锁失败从WAITING --> BLOCKED
- RUNNABLE <--> TIMED_WAITING
- 当前线程调用 t.join(long n) 方法, Thread.sleep(long n) , LockSupport.parkNanos(long nanos) obj.wait(long n)方法 ,就会进入 TIMED_WAITING状态
2. 对于t.join(long n) 方法, Thread.sleep(long n) , LockSupport.parkNanos(long nanos) 当到达指定时间就会从 TIMED_WAITING --> RUNNABLE
对于 obj.wait(long n)方法
- 当t线程等待时间超过了n毫秒,或调用obj.notify(),obj.notifyAll(),t.interrupt() 时
- 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
- RUNNABLE <--> BLOCKED
- 当t线程用synchronized(obj)获取了对象锁时如果竞争失败就会从 RUNNABLE-->BLOCKED
- 持有obj锁线程的同步代码块执行完毕,会唤醒该对向上的所有BLOKED的线程,如果其中t线程竞争成功,从BLOKED-->RUNNABLE ,其他失败的线程仍然是 BLOCKED.
RUNNABLE --> TERMINATED
当当前线程的所有代码执行完成之后,表示线程已经执行完毕,生命周期已经结束了,不会再转换为其他状态.
线程死亡的几种情况
-
线程正常执行完毕:当线程执行完自己的任务后,线程会自然结束并死亡。
-
异常终止:如果线程中抛出未捕获的异常并且没有对该异常进行处理,线程将会异常终止并死亡。这种情况下,可以通过设置异常处理程序来捕获并处理线程中的异常,避免线程死亡。
-
调用
stop()
方法:stop()
方法是一个被废弃的方法,它会强制终止一个线程的执行。这种方式容易引发线程安全问题和资源泄漏,不推荐使用。
线程阻塞的几种情况
- 阻塞状态在Java里面细分为 BLOKED,WAITING,TIMED_WAITING
- BLOKED : 当t线程调用synchronized(obj) 获取对象锁时竞争失败,会进入BLOCKED状态
- WAITING : 当调用线程的join方法,LockSupport.park()方法或者用synchronized(obj)获取了对象锁后,调用obj.wait()方法时.
- TIMED_WAITING : 当用synchronized获取对象锁后,调用wait(long n)方法时候,当调用join(long n) ,调用sleep(long n),或者调用LockSupport的有时限的方法时