目录
3.3 TIMED_WAITING 和 WAITING 以及 BLOCKED
观前提示:本篇博客演示使用的 IDEA 版本为2021.3.3版本,使用的是Java8(又名jdk1.8)
一. 线程的状态
Java线程状态一共有六种,分别为:
1.New(新建)
2.Runnable(可运行)
3.Blocked(阻塞)
4.Waiting(等待)
5.Timed Waiting(计时等待)
6.Terminated(终止)
二. 每种线程状态含义
- New(新建):当一个线程对象被创建但还未调用 start() 方法来启动线程时,线程处于新建状态。此时线程已经分配了内存空间并初始化完毕,但还没有启动它的执行代码。
- Runnable(可运行):线程状态为可运行,表示线程正在 JVM 中执行或等待 CPU 执行。当调用了 start() 方法后,线程就进入了可运行状态。在可运行状态下,线程可能正在等待其他系统资源,如 I/O、网络连接等,并不一定是一直占用 CPU 资源。
- Blocked(阻塞):当线程处于阻塞状态时,表示该线程暂时无法获取所需的锁,因此无法继续执行。常见的原因包括:等待锁、被其他线程调用了 wait() 方法、等待输入/输出(I/O)操作完成等。
- Waiting(等待):当线程处于等待状态时,表示该线程需要等待其他线程通知它去唤醒自己才能继续执行。常见的情况包括:等待其他线程的操作、调用了 wait() 方法而进入等待状态、调用了 join() 方法等待另一个线程执行完毕等。
- Timed Waiting(计时等待):和等待状态类似,但是计时等待状态有超时时间,当等待的时间超过了指定的时间后就会自动恢复到 Runnable 状态。示例包括:调用了 sleep() 方法、调用了 join() 方法并指定了超时时间、调用了 wait() 方法并指定了超时时间等。
- Terminated(终止):线程结束时会进入终止状态,其原因可能是执行完了该线程的任务或者出现了异常而导致线程意外终止。
三. 线程状态之间切换的条件
3.1 NEW 和 TERMINATED
新建状态(NEW):线程已经被创建,但是尚未调用 start() 方法启动线程。这个状态是不能够转移的。
终止状态(TERMINATED):线程已经完成了它的任务或出现了异常而终止。该状态是不能够转移的。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//不打印,只是为了可以让线程运行
}
});
// 在 t.start(); 之前,获取的就是 NEW 状态
System.out.println("start之前: " + t.getState());
t.start();
t.join();
// 线程执行完,TERMINATED 状态
System.out.println("t 结束之后: " + t.getState());
}
通过上面的代码,我们看到了 NEW 状态和 TERMINATED 状态
NEW 状态, 他是在线程对象刚创建的, PCB 还没有创建的时候
TERMINATED 状态, 是 PCB 已经释放,但是线程对象还在.
为什么 PCB 已经释放了,但是线程对象还在呢?因为 Java 中的对象的生命周期,有他自己的规则,他的生命周期和系统内核里的线程并非完全一致,这个内核线程释放的时候,无法保证 Java 代码中 t 对象也立即释放
因此,必然存在,内核的 PCB 没了,但是代码中的 t 还在,此时就需要通过特定的状态,来把 t 对象标识成无效.
3.2 RUNNABLE
运行状态(RUNNABLE):线程正在执行任务或者等待CPU时间片。该状态可以被阻塞状态、等待状态、计时等待状态所转移。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//不打印,只是为了可以让线程运行
}
});
// 在 t.start(); 之前,获取的就是 NEW 状态
System.out.println("start之前: " + t.getState());
t.start();
System.out.println("t 执行的中间状态: "+t.getState());
t.join();
// 线程执行完,TERMINATED 状态
System.out.println("t 结束之后: " + t.getState());
}
新增一段代码,在 t.start 和 t.join 中间,用来看 RUNNABLE
RUNNABLE 代表可能在 CPU 执行,也可能在排队,这不重要,我们只需要知道他在 RUNNABLE
3.3 TIMED_WAITING 和 WAITING 以及 BLOCKED
计时等待状态(TIMED_WAITING):线程因为等待某些条件而被暂停,但是有一个超时时间。当等待的时间超过了指定的时间后就会自动恢复到Runnable状态。该状态可以被阻塞状态、等待状态或终止状态所转移。
等待状态(WAITING):线程因为等待某些条件而被暂停,需要其他线程通知唤醒该线程。该状态可以被计时等待状态、阻塞状态或终止状态所转移。
阻塞状态(BLOCKED):线程因为某种原因被暂停,无法执行。该状态可以转移到运行状态或终止状态。
Java线程进入阻塞状态的原因大致可以分为以下三类:
-
等待阻塞:调用Object.wait()方法或wait(long timeout)方法后线程会进入等待阻塞状态。当等待时间结束或者其他线程调用该对象的notify()或notifyAll()方法时,线程才会被唤醒。
-
同步阻塞:当线程请求获取某个对象的锁,而该锁被其他线程占用时,线程就会进入同步阻塞状态,直到获得锁才能继续执行。
-
其他原因导致的阻塞:例如调用了Thread.sleep()方法,或者调用了一些阻塞式IO操作,如读取数据等,线程也会进入阻塞状态。
3.3.1 join 方法和 sleep 方法
现在演示使用 join 方法和 sleep 方法使线程进入阻塞状态
在先前的代码中 for 循环里面是空的
for (int i = 0; i < 1000; i++) {
//不打印,只是为了可以让线程运行
}
现在我们把 sleep 加进去 看看是什么效果
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//不打印,只是为了可以让线程运行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 在 t.start(); 之前,获取的就是 NEW 状态
System.out.println("start之前: " + t.getState());
t.start();
System.out.println("t 执行的中间状态: "+t.getState());
t.join();
// 线程执行完,TERMINATED 状态
System.out.println("t 结束之后: " + t.getState());
}
我们可以看到,加入 sleep 以后,代码依然运行,显示的是 RUNNABLE (其实到底是显示 RUNNABLE 还是 TIMED_WAITING 就不一定,取决于代码的具体实现)此处代码的实现是一千个循环,每次循环休眠一秒,所以一直看到 RUNNABLE
通过循环获取,能够看到这里的交替状态
当前获取到的状态,到底是处在什么状态,执行状态还是 sleep 状态,取决于系统的调度操作
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
int a = 10;
a += 10;
}
});
// 在 t.start(); 之前,获取的就是 NEW 状态
System.out.println("start之前: " + t.getState());
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("t 执行的中间状态: " + t.getState());
}
t.join();
// 线程执行完,TERMINATED 状态
System.out.println("t 结束之后: " + t.getState());
}
3.3.2 wait 方法
先来简单认识一下 wait 方法
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object){
System.out.println("wait 之前");
object.wait();
System.out.println("wait 之后");
}
}
那个线程调用 wait 方法,那个线程就会进行阻塞,此时他就处于 WAITING
3.3.2.1 JConsole
我们打开 JConsole 查看自己的线程
JConsole 是 idea 安装的时候就有的,如果找不到可以直接打开Win10的搜索
然后输入: JConsole,然后点击运行
我的进程名字是 ThreadWait 所以链接这个
这里直接点击不安全的链接
找到我们的 main 线程,可以看到 WAITING
要使用 wait 方法,就必须搭配使用 synchronized 方法,否则就会抛出异常
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait 之前");
object.wait();
System.out.println("wait 之后");
}
这个异常就是,"非法监视器" 异常 ,为什么会出现这个异常呢?
因为: wait 方法的操作分为三步
第一步:先释放锁
第二步:进行阻塞等待
第三步:收到通知之后,重新尝试获取锁,并且在获取锁之后,继续往下执行
而 synchronized 方法,就是进行加锁,如果没有 synchronized 方法,即是没有进行加锁,也就是无法满足 wait 方法的第一步:获取锁,自然也就无法继续进行后续的步骤.
synchronized 方法的括号里面放的是你要进行加锁的线程,比如上面的代码, 里面放的是 object
举个例子就是:你要用电脑进行学习,第一步是打开电脑,不打开电脑,后续步骤都无法进行.
3.3.2.2 synchronized 方法
这里有两段代码可以进一步帮助理解 synchronized 方法加的锁
class Counter {
public int count;
public void add() {
count++;
}
public void add2() {
count++;
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Counter counter = new Counter();
//两个线程,分别调用5w次add
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add2();
}
});
//启动
t1.start();
t2.start();
//等待结束
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//打印结果
System.out.println("Counter = " + counter.count);
}
这段代码是不使用 synchronized 方法,让两个线程各自自增五万次在相加
每次自增得到的结果都不尽相同
下面这段代码是使用 synchronized 方法以后,再让两个线程各自自增五万次在相加
每次的结果就都是相同的
class Counter2 {
public int count2;
synchronized public void add2() {
count2++;
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Counter2 counter2 = new Counter2();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter2.add2();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter2.add2();
}
});
t1.start();
t2.start();
// 防止 main 线程运行两次 start 后,立即结束计时
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Counter = " + counter2.count2);
}
}
上面两端代码的区别就是:是否使用了 synchronized 方法
为什么使用 synchronized 方法以后就会每次都是十万呢?为什么不使用 synchronized 方法就每次得到的答案都是不稳定的呢?
因为不使用 synchronized 方法就要面临多线程带来的风险-线程安全
万恶之源就是多线程的抢占式执行,带来的随机性
如果没有多线程,代码就是固定的一条线往下执行,结果都是固定的.
但是当使用了多线程以后,抢占式执行就会让代码的执行出现变数,代码的执行就不再是一条直线,而是多出了无数种可能性,只要有一种情况下代码出现第二种结果,就视为有 bug, 线程不安全
那么,是否可以解决这种不稳定性呢?
答案是不行,首先:调度的源头来自操作系统的内核你改不了,其次:假设你是真的可以改内核,别人认可你改的东西吗?
然后我们再来看为什么这个十万相加的代码会出现线程安全问题
这是因为 add 方法他不是一个整体,我们看着他就是一个相加,但是他的本质是需要分成三步
1.load: 先把内存中的数据,读取到 CPU 的寄存器中
2.add: 把 CPU 寄存器中的数值进行 +1 运算
3.save: 把得到的结果写回到内存中
当两个线程并发执行 count++,此时就相当于, 两组 add, load, save 进行执行
此时不同的线程调度,就会导致结果出现偏差
下面画图进行详细解释
并发执行带来的抢占式执行不止上面说的这一种扰乱方式,还有很多种,具体可以自己尝试排列组合
为了解决抢占式执行带来的线程安全问题,我们就需要使用 synchronized 方法
他会直接进行上锁(lock)
注意:这里加锁,是保证了原子性,并不是说让三个操作一次性完成,也不是这三个操作过程不进行调度,而是让其他也想操作的线程阻塞等待了
下面的图更符合上面这段话
ps:要想上面的代码能成功运行,不光需要使用 synchronized 方法, wait 方法, notify 方法,还需要注意一点
这四个标注的地方必须是相同的对象,比如都是 object,才能正常运行,因为 notify 方法只能唤醒同一个对象上等待的线程
关于 synchronized 方法还有很多其他的东西,就不在这篇博客详细描述了,后面的博客继续讲
3.3.3 notify 方法
因为没有唤醒程序,所以 wait 方法这里是在死等
因此我们需要一个唤醒程序 -> notify 方法
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(() -> {
System.out.println("t1 wait 之前");
synchronized (object) {
try {
object.wait();
// wait() 方法可以使当前线程释放锁,并进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 wait 之后");
}
});
Thread t2 = new Thread(() -> {
System.out.println("t2 notify 之前");
synchronized (object) {
object.notify();
//notify() 方法则是唤醒在等待队列中等待的线程,使其进入锁池竞争,但不会立即释放锁
}
System.out.println("t2 notify 之后");
});
t1.start();
Thread.sleep(500);
// 如果不进行 sleep 就会概率出现t2先运行,t1后运行,导致死等
t2.start();
}
此时代码的运行效果就符合我们要的效果
3.3.4 join 方法 和 wait 方法的使用场景
join() 方法和 wait() 方法都是线程控制相关的方法,但是它们的作用有所不同。
- 原理区别
join() 方法是让调用该方法的线程等待另一个线程执行完毕,也就是将当前线程加入到另一个线程中,等待另一个线程执行完成后再继续执行。而 wait() 方法是让一个线程等待另一个线程的通知,需要在 synchronized 块或方法中调用 wait(),释放锁进入等待状态,直到其他线程调用 notify() 或 notifyAll() 方法时才会唤醒该线程。
- 使用场景区别
join() 方法常用于主线程等待子线程执行完毕后再继续执行,可以保证多个线程执行顺序的协调。而 wait() 方法用于多个线程之间的协作,例如实现生产者–消费者模型、定时任务等。
- 锁对象区别
join() 方法是对线程对象进行操作,等待另一个线程执行完毕后再继续执行,不需要对锁对象进行处理。而 wait() 方法需要在 synchronized 块或方法中调用,并且需指定监视器对象,即锁对象,其他线程需要使用相同的锁对象才能调用 notify() 或 notifyAll() 方法,唤醒被等待的线程。
- 调用方式与参数
join() 方法是线程对象的方法,需要通过线程对象来调用,可以使用 join(long millis) 方法指定等待时间,也可以不指定等待时间。而 wait() 方法是 Object 类中的方法,可以在任何对象上调用,需要指定等待时间,并且需要使用 synchronized 块或方法来进行同步。
综上所述,join() 方法和 wait() 方法虽然都是线程控制相关的方法,但是它们的作用、使用场景、锁对象以及调用方式等方面都有所不同,需要根据具体情况进行选择使用。
如果你想要 t1 彻底执行完,再去执行 t2, 就使用 join 方法
如果你想要 t1 执行一半,就去执行 t2, 就使用 wait 方法
3.3.5 sleep 方法 和 wait 方法区别
sleep() 方法和 wait() 方法都可以让线程等待一段时间,但是它们的作用不同,主要区别如下:
-
调用 wait() 方法会释放对象锁,使线程进入等待状态,直到其他线程调用该对象的 notify() 或者 notifyAll() 方法才能唤醒该线程,而调用 sleep() 方法只是让线程休眠一段时间,并不会释放对象锁。
-
wait() 方法必须在 synchronized 块或者方法中调用,而 sleep方法可以在任何地方使用。
-
如果调用 wait() 方法的线程未获取到对象锁,则会抛出 IllegalMonitorStateException 异常,而调用 sleep() 方法则不会。
-
wait() 方法不需要指定等待的时间,线程会一直等待直到其他线程调用 notify() 或者 notifyAll() 方法,而 sleep() 方法则需要指定等待的时间。
综上所述,sleep() 方法和 wait() 方法虽然都能让线程等待一段时间,但是它们的机制和用法有很大的不同,应该根据具体的场景进行选择使用。
ps: sleep 也可以被提前唤醒, interrupt 唤醒 sleep 则是出现了异常(表示一个出问题的逻辑)
3.3.6 notify 和 notifyall的区别
notify() 和 notifyAll() 都是 Java 中 Object 类的方法,可以用于唤醒正在等待某个对象监视器(即锁)的线程。它们的区别如下:
-
notify() 方法只会唤醒一个等待该对象监视器的线程,并将该线程从等待池中移到同步队列中等待获取对象的锁。如果同步队列中没有线程在等待该对象的锁,则 notify() 操作不会有任何效果。 而 notifyAll() 方法会唤醒所有等待该对象监视器的线程,并将它们从等待池中移到同步队列中等待获取对象的锁。
-
由于 notify() 方法只唤醒一个等待线程,因此唤醒的线程可能不是当前最需要执行的线程,这就可能导致某些线程因得不到唤醒而一直等待下去,降低程序的效率。而使用 notifyAll() 方法则能够确保所有等待线程都被唤醒,从而提高程序并发性能。
-
在多个线程等待同一个对象监视器时,notify() 方法可能产生“惊群效应”(thundering herd problem),即多个线程同时被唤醒,但只有一个线程能够获得对象锁,其他线程又进入了等待状态,降低了程序的并发性能。而使用 notifyAll() 方法可以避免“惊群效应”,因为所有等待线程都会被唤醒,但只有一个线程能够获得对象锁。
综上所述,notify() 和 notifyAll() 都可以用于唤醒等待某个对象监视器的线程,但区别在于前者只唤醒一个线程,后者则唤醒所有等待线程。在实际编程中,需要根据具体情况来选择使用哪一种方法来提高程序的并发性能。
以上就是多线程的六种状态,每种状态的含义,状态之间的切换条件,以及锁的使用(简单介绍)
本文完,感谢观看,有什么错误和不足的地方请在评论区指出,一起进步,谢谢 !