🎇个人主页 :Ice_Sugar_7
🎇所属专栏 :JavaEE
🎇欢迎点赞收藏加关注哦!
线程状态
🍉start 和 run 的区别
这是一个经典的面试题,以下面代码为例:
java
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello");
}
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
t.run();
}
}
可以看到结果都输出"hello"
这两者的区别在于:
调用start
是 创建一个新的线程**,由这个线程执行打印 hello 的任务;**
而 t.run()
则是调用 Thread 实例中的 run 方法,这个操作是在 main 主线程中打印 hello
如果我们把代码改成下面这样:在 run 方法和 main 方法中写个死循环,此时 t.run() 就只打印 hello thread,主线程没办法再向下执行
java
public class MyThread extends Thread{
@Override
public void run() {
while(true) {
System.out.println("hello thread");
}
}
public static void main(String[] args) {
Thread t = new MyThread();
t.run();
while(true) {
System.out.println("hello main");
}
}
}
🍉终止线程
一个线程,它的 run 方法如果执行完毕,那么它就终止了
如果我们想让线程提前终止,那就需要让 run 方法能够提前结束。我们一般会引入标志位
,在其他进程中修改标志位的值
来结束进程
也就是说:线程 A 什么时候结束,取决于另一个线程 B 什么时候修改 A 的标志位的值
Thread 实例提供的 currentThread 方法
可以用来获取当前线程实例。也就是说哪个线程调用这个方法,得到的就是哪个线程的实例 (类似 this)
比如下面这个代码,我们先看 while 循环
java
while(!Thread.currentThread().isInterrupted()) {
//...
}
isInterrupted 方法是用来查看当前线程是否被中断。如果一个线程被中断,那么得到的结果就为 true,它其实就相当于标志位
通过实例.interrupt()
可以中断线程
java
public class MyThread{
public static void main(String[] args) throws InterruptedException{
Thread t = new Thread(()-> {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("线程运行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程执行完毕");
});
t.start();
Thread.sleep(3000);
//让 t 线程结束
t.interrupt();
}
}
看下结果:
可以看到代码出现异常之后,t 线程还在打印,这说明它并没有真正结束
而如果删掉匿名内部类中的 sleep,那么 interrupt 可以让线程顺利结束:
java
public static void main(String[] args) throws InterruptedException{
Thread t = new Thread(()-> {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("线程运行中");
}
System.out.println("线程执行完毕");
});
t.start();
Thread.sleep(2000);
t.interrupt();
}
那就说明 sleep 导致结果和预期结果不同
在执行 sleep 的过程中,调用 interrupt,可能会导致 sleep 的休眠时间还没到,就被提前唤醒
了
被提前唤醒后,会做两件事:
①抛出 InterruptedException (这个异常紧接着就会被 catch 捕获到)
②清除 Thread 对象的 isInterrupted 标志位
在上面的代码中,我们已经通过 Interrupt 方法把标志位设为 true 了,但是 sleep 被提前唤醒后就把标志位设回 false,所以导致循环继续执行
如果想让线程结束,只需在 catch 中加上 break 就 ok 了:
java
while(!Thread.currentThread().isInterrupted()) {
System.out.println("线程运行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
当然,其实不止 sleep 有清空标志位的机制,很多方法都会这样
清空标志位是为了给我们更多的操作空间
比如上一行代码写的是 sleep(1000),但是现在1000ms 还没到就要终止线程,这样就前后矛盾了,此时就需要抛出异常,然后对这种情况进行具体的处理
我们可以在 catch 语句中加入一些代码来做处理:
- 让线程立即结束:加上 break
- 让线程继续执行:不加 break
- 让线程执行一些逻辑之后再结束:写一些其他代码,再 break
idea 生成的 catch 语句里面自动给的代码是 e.printStackTrace(),这个是在打印调用栈,或者是抛出另外一个异常。实际开发中这两种代码只是纯纯占个位置而已,没啥卵用
🍉join & 阻塞状态
虽然多个线程之间的执行顺序是不确定的,但是我们可以在应用程序中通过一些 api 来影响线程执行的顺序
join 就是一种方式,也是线程最核心的 api 之一,它通过影响线程结束的先后顺序来影响总的执行顺序
比如让 main 线程等待 t 线程,那就在 main 线程中调用 t.join()
执行 join 的时候,会看 t 线程是否正在运行
如果 t 正在运行,那么 main 线程就会阻塞(暂时不参与 CPU 执行)
如果 t 运行结束,那么 main 就会从阻塞中恢复过来,继续向下执行
由此可以看出:阻塞使这两个线程的结束时间产生先后顺序
在上面的例子中,就一定是 t 先结束,然后才是 main 结束
实际开发中一般不止 t 和 main 这两个线程,t 线程虽然可能是和其他线程共同进行调度的,但由于主线程一直处于等待状态,所以即使 t 中间经历多次 CPU 的切换,最终也能顺利执行完毕
join 的一个典型应用就是使用多个线程并发进行一系列计算,让一个线程阻塞等待上述计算线程,等到所有线程都计算完了,再让这个线程汇总结果
举个例子,弄两个线程,合作计算 1 到 100w 的和(一个线程计算 50w 个数),最后在 main 线程中打印结果:
java
public class MyThread{
public static long sum = 0;
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()-> {
for(long i = 1;i <= 50_0000L;i++) sum += i;
});
Thread t2 = new Thread(()-> {
for(long i = 50_0001L;i <= 100_0000L;i++) sum += i;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(sum);
}
}
不过这个结果貌似不太对,因为两次算出来的 sum 不一样,这就涉及到后面要讲的线程安全问题,不过这是后话,现在只需知道弄多个线程分别运算的效率,会比单独一个线程运算的效率高就 ok 了
然后我们在调用 join 的时候,可以看到它其实有三个重载的方法
millis 和 nanos 分别是毫秒和纳秒,不过因为系统的时间没法精确到纳秒级别,所以没啥卵用
如果参数不填时间(也就是第一个重载方法),那称为"死等"
,就是说某个线程一定要等到另一个线程执行完才会继续向下执行 。但是这种逻辑其实是不科学
的,因为如果代码中因为死等导致程序卡住了,那就无法处理后续的逻辑,这就是非常严重的 bug 了
如果参数填了时间,那则是带有超时时间
的等待,如果等待的时间超过超时时间,那就不会再等了,继续执行
🍉线程六大状态
Java 中线程的状态可分为:
- NEW:已经创建好了 Thread 对象,但是还没有调用 start 方法在系统中创建线程 (只有处于 NEW 状态才能 start,并且一个 Thread 对象只能 start 一次)
- TERMINATED:系统内部的线程执行完毕
- RUNNABLE:就绪状态,表示这个线程正在 CPU 上执行,或者随时都可以去 CPU 上执行
- TIMED_WAITING:指定时间的阻塞,到达一定时间之后会自动解除阻塞,使用 sleep 或 带有超时时间的 join 会进入这个状态
- WAITING:不带时间的阻塞(死等),必须满足一定条件才会解除阻塞,使用 join 或者 wait 会进入这个状态
- BLOCKED:由于锁竞争引起的阻塞(后面说到线程安全时会详细介绍)
可以用一幅图来表示这六个状态间的联系:
这些状态在我们调试多线程代码的 bug 时可以作为重要参考依据
比如我们常说"程序卡住了",这就说明一些关键的线程出现阻塞,我们可以通过观察线程的状态分析出一些原因