线程状态
对于Java
中线程状态,JVM
有明确声明:虚拟机中的线程状态,不反应任何操作系统中的线程状态。JVM
在设计上有自己的一套规范,切勿与操作系统底层的线程状态混为一谈。
Java
线程状态使用了Thread
的内部类State
来表示,规定了如下的六种状态;
状态 | 含义 |
NEW | 新创建了一个线程对象,但还没有调用start()方法 |
RUNNABLE | 可运行状态,线程对象调用 |
BLOCKED | 阻塞状态,表示线程正在等待一个监视器锁(monitor lock),而监视器锁在 |
WAITING | 等待状态,表示线程在等待某些条件的到达 |
TIMED_WAITING | 超时等待状态,与WAITING状态类似,并在其基础上,增加了超时的限制 |
TERMINATED | 终止状态,包括线程正常执行完毕和异常终止 |
线程状态间的流转如下:
RUNNABLE
-
RUNNABLE
状态表示正在运行或者准备运行,这种状态是相对于JVM
来说的,而线程具体何时运行,这取决于操作系统底层的资源调度;注意在操作系统底层,正在运行和准备运行是两种不同的状态,但是JVM
层面并不细分这两个 - 当执行
yield
操作时,底层状态确实是有变化的,线程会让出当前执行权,让CPU可以去执行其他可运行的线程,这是底层的变化,但是JVM
层面,原线程依旧还是RUNNABLE
状态
BLOCKED 与 WAITING 状态的区别
-
BLOCKED
状态表示正在等待一个监视器锁,即虚拟机认为程序还不能进入某个区域,因为同时进去就会有问题,这是一块临界区 -
WAITING
状态的先决条件则是已经进入临界区,也就是说线程已经拿到锁了,但是因为其他原因,在此等待一下 - 简单来说,线程只有在等待进入
synchronized
代码块或者方法时,才会进入BLOCKED
状态;此外,注意对于Lock
接口下实现的锁,由于其底层都是调用LockSupport
中的方法,因此等待锁时进入的都是WAITING
或者TIMED_WAITING
线程方法
线程等待(wait)
-
wait()
方法位于Object
类中,前面说了进入等待状态的前提是已经拿到对象的锁了,所以该方法必须在同步代码中才能使用 - 调用该方法,当前线程进入
WAITING
状态,只有等待其他线程的通知,或者当前线程被中断才会返回 - 调用
wait()
方法,线程会释放锁;因为如果不释放锁,那么其他线程就无法获取当前对象的锁,那么也就无法执行notify/notifyAll
方法来唤醒挂起的线程,这就造成死锁了
线程睡眠(sleep)
-
sleep()
方法位于Thread
类中 -
sleep()
方法不会释放当前对象的锁,只能等待时间自然醒来执行完,其他线程才能继续获取当前对象的锁
wait 与 sleep 的区别?
- 这两方法来自不同的类中,
wait()
作用于资源对象本身,sleep()
方法作用于当前线程 - 最主要的就是
wait()
方法会释放锁,而sleep()
不会释放锁 - 使用范围不同,
wait()
必须在同步代码中使用,sleep()
可在任何地方使用(这也印证了第二点,sleep
没有锁可以释放)
线程让步(yield)
-
yield
会使当前线程让出 CPU 执行时间片,即暂停一下,让系统的线程调度器重新选择个可运行的线程执行 - 注意当前线程调用
yield
方法后,最终选择的结果可能还是当前线程继续执行;一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感
线程中断(interrupt)
- 注意线程中断并不是立刻停止线程工作的意思,因为强制停止某个线程运行可能会造成无法预估的后果,但是某些场景是需要停止线程的,而线程中断就可满足这些使用场景;它的使用场景类似如下:在
A
线程中调用B
线程的interupt()
方法,即相当于跟B
线程打了个招呼,并给B
线程打上了中断标记,在B
线程中可调用isInterrupted()
或者interrupted()
方法来判断是否有其他线程想中断自己,根据这个判断结果,B
线程可以自行决定自己接下来要怎么做 - 线程中断最重要的就是如下三个方法:
-
interupt()
:中断目标线程,给目标线程发一个中断信号,目标线程被打上中断标记 -
isInterrupted()
:判断线程是否被中断,不会清除中断标记 -
interrupted()
:判断线程是否被中断,会清除中断标记
- 调用线程中断时,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出
InterruptedException
,并且会清除打断标识;但是不能中断 I/O 阻塞和 synchronized 锁阻塞 - 注意线程中断是线程之间的一种协作机制,如果不明白线程在做什么,不应该贸然的调用线程的
interrupt
方法,以为这样就能取消线程
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(() -> {
// 判断是否被中断
while (!Thread.currentThread().isInterrupted()) {
System.out.println(" A:学习java中...");
}
System.out.println("A:听说有人想中断我?学完js再说");
// 同样返回中断标识, 并且会清除标识
boolean interrupted = Thread.interrupted();
System.out.println("A:当前中断标识:" + interrupted + ", 清除掉,忽略继续学习");
boolean b = Thread.currentThread().isInterrupted();
System.out.println("A:清除掉之后的标识:" + b + ", 可以安心学习了");
// 判断是否被中断
while (!Thread.currentThread().isInterrupted()) {
System.out.println(" A:学习js中...");
}
System.out.println("A:又想中断我, 那休息下吧");
}, "A");
A.start();
try {TimeUnit.NANOSECONDS.sleep(1);} catch (InterruptedException e) {}
// 中断A线程, 不要沉迷学习了
A.interrupt();
try {TimeUnit.NANOSECONDS.sleep(1);} catch (InterruptedException e) {}
// 继续中断
A.interrupt();
}
执行结果:
A:学习java中...
A:学习java中...
A:学习java中...
A:学习java中...
A:听说有人想中断我?学完js再说
A:当前中断标识:true, 清除掉,忽略继续学习
A:清除掉之后的标识:false, 可以安心学习了
A:学习js中...
A:学习js中...
A:学习js中...
...
A:又想中断我, 那休息下吧
线程等待(join)
- 使用场景如下:在
A
线程中调用B
线程的join()
方法,则A
线程必须等待B
线程执行完毕才继续执行 - 注意调用
B
线程的join()
方法时,是需要B
线程已启动,否则无效的,这点从join()
方法源码中很容易看出
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(() -> {
System.out.println("a开始执行");
sleep(3);
System.out.println("a执行完毕");
});
Thread b = new Thread(() -> {
try {
System.out.println(" b开始执行");
a.join();
System.out.println(" b执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
a.start();
sleep(1);
b.start();
}
执行结果:
a开始执行
b开始执行
a执行完毕
b执行完成
线程唤醒(notify/notifyAll)
- 通常与
wait()
方法搭配使用,用来唤醒等待状态的线程继续执行;该同样位于Object
类中 - 注意调用
notify/notifyAll
方法并不会释放对象锁
wait/notify搭配使用
大致流程如下:
代码示例:
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread A = new Thread(() -> {
synchronized (obj) {
System.out.println("A:先拿到锁, 玩两秒");
sleep(2);
try {
System.out.println("A:进入等待队列");
obj.wait();
System.out.println("A:被唤醒, 拜拜");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A");
Thread B = new Thread(() -> {
System.out.println(" B:锁被A占据, 先等着");
synchronized (obj) {
System.out.println(" B:线程A等待去了,我拿到锁了, 玩两秒");
sleep(2);
System.out.println(" B:唤醒A");
obj.notify();
System.out.println(" B:虽然我唤醒A, 但我接着玩, A没拿到锁, 仍无法执行, A线程状态:" + A.getState());
sleep(2);
System.out.println(" B:玩完了, 拜拜");
}
}, "B");
A.start();
sleep(1);
B.start();
}
private static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
执行结果:
A:先拿到锁, 玩两秒
B:锁被A占据, 先等着
A:进入等待队列
B:线程A等待去了,我拿到锁了, 玩两秒
B:唤醒A
B:虽然我唤醒A, 但我接着玩, A没拿到锁, 仍无法执行, A线程状态:BLOCKED
B:玩完了, 拜拜
A:被唤醒, 拜拜
死锁
示例:
死锁描述的是多个线程都因为想获取对象锁而进入阻塞状态,且这种状态无法自己恢复过来,无限期的阻塞;
如下图所示,线程A、B都因为想获取某个对象锁而阻塞住,但是却又无法获取到,仿若形成一个圈,陷入死循环
代码示例:
public static void main(String[] args) throws InterruptedException {
Object obj1 = new Object();
Object obj2 = new Object();
Thread A = new Thread(() -> {
synchronized (obj1) {
System.out.println("A获取到 obj1 的锁");
sleep(2);
System.out.println("A想获取到 obj2 的锁");
synchronized (obj2) {
System.out.println("A已获取到 obj2 的锁");
}
System.out.println("A 不获取了");
}
}, "A");
Thread B = new Thread(() -> {
synchronized (obj2) {
System.out.println("B 获取到 obj2 的锁");
sleep(2);
System.out.println("B 想获取到 obj1 的锁");
synchronized (obj1) {
System.out.println("B 已获取到 obj1 的锁");
}
System.out.println("B 不获取了");
}
}, "B");
A.start();
B.start();
}
运行结果:
A获取到 obj1 的锁
B 获取到 obj2 的锁
A想获取到 obj2 的锁
B 想获取到 obj1 的锁
此时会发现程序并未运行完,且一直不会结束,容易看出线程A卡在想获取obj2
的锁上,线程B与之相反,这就是死锁了,无法自己恢复过来了
八股文之产生死锁的四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
八股文之避免死锁:
那就是破坏产生死锁的四个条件之一即可;通俗来讲,可有如下做法:
- 按照顺序来获取资源,既然线程A是先获取
obj1
,再想获取obj2
,那么线程B也可以按照这个顺序来,那就不会死锁了 - 主动释放资源,线程A可选择先尝试获取
obj2
的锁,若没有获取到,那么该咋样就咋样,别阻塞了,继而释放自己占据的obj1
的锁 - 一次性申请所有资源,线程A可以一开始就将
obj1
、obj2
的锁都获取到,最后再都释放掉,也不会死锁了