线程的状态
Java中线程的状态分为 6 种:
- 初始(NEW):新建了一个线程对象,但是还没调用start() 方法
- 运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种状态统称为“运行”。线程对象在创建之后,其他线程(比如 main 线程)调用了该对象的 start() 方法,该状态的线程位于可变线程池中,等待被线程调度选中,获取CPU 的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得 CPU时间片后变为运行状态(running)。
- 阻塞(BLOCKED):表示线程阻塞与锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定的动作(通知或者中断)。
- 超时等待(TIMED_WAITING):该状态不同于 WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
wait
使用场景
获取到某个线程之后,发现当前并不需要使用该线程,则可以调用wait() 方法,使其进入等待过程。
等到某个时刻,满足条件之后,则可以使用notify() 或者 notifyAll() 方法来唤醒这个线程。
使用条件
对于已经获取到锁的线程才可以调用锁的wait(),notify()方法,否则会抛出IllegalMonitorStateException 异常。
比如下面的代码,A获得了锁之后,主动调用 wait() 方法释放锁和CPU资源,然后就进入了阻塞状态。
主线程在没有获得锁的情况之下,直接调用notify() 方法会抛出异常。
public class WaitTest {public static void main(String[] args) {Object lock = new Object();Thread threadA = new Thread(() -> {synchronized (lock) {System.out.println("获取到锁");try {System.out.println("休眠一会");TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("调用wait");try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("被唤醒");}}, "A");threadA.start();lock.notify();}
}
输出结果:
现在我们创建一个线程B,抢占锁后,再调用 notify() 方法
public class WaitTest {public static void main(String[] args) {Object lock = new Object();Thread threadA = new Thread(() -> {synchronized (lock) {System.out.println("A 获取到锁");try {System.out.println("休眠一会");TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("调用wait");try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("A 被唤醒");}}, "A");threadA.start();Thread threadB = new Thread(() -> {synchronized (lock) {System.out.println("B 获取了锁");System.out.println("B 唤醒 A");lock.notify();}},"B");threadB.start();}
}
线程状态变化
首先我们需要知道,线程正常运行时的状态是 RUNNABLE,调用 wait() 方法后,变为 WAITING 状态。
举例说明:
public class WaitTest {public static void main(String[] args) {Object lock = new Object();Thread threadA = new Thread(() -> {synchronized (lock) {System.out.println("A 获取到锁");try {System.out.println("休眠一会");TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("调用wait");try {System.out.println("调用wait前的线程状态:"+Thread.currentThread().getState());lock.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("A 被唤醒");}}, "A");threadA.start();try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("调用wait后的线程状态:"+threadA.getState());}
}
输出结果:
提问:主动 wait 的线程,被唤醒之后,它的状态一定是从 WAITING 变成 RUNNABLE 吗?
我们再做一个测试:A线程wait,2秒后,启动B线程,B线程去唤醒A线程。记录下唤醒前后的状态。
public class WaitTest {public static void main(String[] args) {Object lock = new Object();Thread threadA = new Thread(() -> {synchronized (lock) {System.out.println("A 获取到锁");try {System.out.println("休眠一会");TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("调用wait");try {System.out.println("调用wait前的线程状态:"+Thread.currentThread().getState());lock.wait();System.out.println("调用notify后的线程状态:"+Thread.currentThread().getState());} catch (InterruptedException e) {e.printStackTrace();}System.out.println("A 被唤醒");}}, "A");threadA.start();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}Thread threadB = new Thread(() -> {synchronized (lock) {System.out.println("B 获取到锁");System.out.println("叫醒A前,A的状态:"+threadA.getState());System.out.println("叫醒A");lock.notify();}},"B");threadB.start();}
}
输出结果:
上面的例子与我们的猜测是一致的。
但是,这段代码并不严谨,上述代码的 B线程在调用了 notify() 方法之后,立即释放了锁,但假如 B线程调用 notify() 后,还有很多任务没有完成,不立即释放锁,A线程的状态又会有什么变化?
修改后的代码:
public class WaitTest {public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread threadA = new Thread(() -> {synchronized (lock) {System.out.println("A 获取到锁");try {System.out.println("休眠一会");TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("调用wait");try {System.out.println("调用wait前的线程状态:"+Thread.currentThread().getState());lock.wait();System.out.println("调用notify后的线程状态:"+Thread.currentThread().getState());} catch (InterruptedException e) {e.printStackTrace();}System.out.println("A 被唤醒");}}, "A");threadA.start();TimeUnit.SECONDS.sleep(2);Thread threadB = new Thread(() -> {synchronized (lock) {System.out.println("B 获取到锁");System.out.println("叫醒A前,A的状态:"+threadA.getState());System.out.println("叫醒A");lock.notify();System.out.println("发现还有很多事要做,先不释放锁");System.out.println("B 在做其他事的时候,A 的状态:"+threadA.getState());try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("B 的事情都做完了");}},"B");threadB.start();}
}
输出结果:
通过观察我们发现,A 的状态并没有立刻变成 RUNNALBE,而是 BLOCKED,等到 B线程任务处理完释放锁之后,A 的状态才变成 RUNNALBE。
也就是说,B调用完 notify() 方法后,并不会立刻把线程的控制权交出去,A 被唤醒后,也不会立即将状态变成 RUNNABLE,而是先变成 BLOCKED,然后参与锁竞争,竞争成功重新获得锁之后,再向下执行。
Java中,每一个对象都有一个对应的 Monitor 锁,Monitor维护着 EntrySet 和 WaitSet。线程阻塞时,对象会被放入到 EntrySet 中,对应的状态是 BLOCKED。线程调用wait方法后,会被加入到WaitSet中,对应的状态是WAITING、TIMED_WAITIING。
上面我们分析过,线程“被唤醒”和“获得锁”是两个过程,被唤醒的线程需要重新参与锁竞争。
这么理解的话,那么线程应该是从 WaitSet 中苏醒后,又被加入到 EntrySet 队列。
有限等待
wait() 方法可以传入一个时间参数,是一种自动唤醒机制:在指定的时间内,如果没有其他线程唤醒自己,则主动唤醒自己。传 0 或者不传,则表示永久等待。
public class WaitTest {public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread threadA = new Thread(() -> {synchronized (lock) {System.out.println("A 获取到锁");try {System.out.println("休眠一会");TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("调用wait");try {System.out.println("调用wait前的线程状态:"+Thread.currentThread().getState());System.out.println("当前时间:"+System.currentTimeMillis());lock.wait(1000);System.out.println("调用notify后的线程状态:"+Thread.currentThread().getState());System.out.println("当前时间:"+System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();}System.out.println("A 被唤醒");}}, "A");threadA.start();}
}
输出结果:
这只是一种情况,如果 A 被唤醒后,继续参加锁的竞争,假设 A 在wait(t) 的过程中,有其他线程抢占了线程,A还能继续执行吗?
以下是代码示例:
public class WaitTest {public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread threadA = new Thread(() -> {synchronized (lock) {System.out.println("A 获取到锁");try {System.out.println("休眠一会");TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("调用wait");try {System.out.println("调用wait前的线程状态:"+Thread.currentThread().getState());System.out.println("当前时间:"+System.currentTimeMillis());lock.wait(100);System.out.println("调用notify后的线程状态:"+Thread.currentThread().getState());System.out.println("当前时间:"+System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();}System.out.println("A 被唤醒");}}, "A");threadA.start();Thread threadB = new Thread(() -> {synchronized (lock) {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("A 的状态:"+threadA.getState());while (true) {}}},"B");threadB.start();}
}
输出结果:
即使 1000ms 过了,A 也不能继续执行,因为 A 只是被唤醒了,但是并没有成功获得锁,所以会进入 BLOCKED 状态。
sleep
Java中的线程休眠可以使用 Thread 类的静态方法 sleep()实现,该方法可以让当前线程暂停执行一段时间,以等待其他线程完成某些操作,或者为了节省系统资源而暂停线程执行。
代码示例:
public class SleepThreadExample {public static void main(String[] args) {System.out.println("线程开始执行");try {Thread.sleep(5000); // 休眠5秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程结束执行");}
}
在上面的代码中,我们使用sleep() 方法让线程休眠5 秒,然后再继续执行。如果在休眠期间线程被终端,就会抛出 InterruptedException异常。
需要注意的是,sleep() 方法会暂停当前线程的执行,因此如果在主线程中调用该方法,就会导致整个程序暂停执行,直到指定的时间到达。因此,我们应该避免在主线程中过渡使用sleep()方法,以免影响程序的响应性和用户体验。
sleep和wait的异同点
相同点
- 都可以使线程堵塞
- 都可以响应中断
不同点
- wait、notify方法必须写在同步方法中,是为了防止死锁和永久等待,使线程更安全,而sleep方法不需要有这个限制。
- wait方法调用后会释放锁,sleep方法调用后不会释放锁。
- sleep方法必须要指定时间参数,wait方法可以指定时间参数,但不是必需的
- 两个方法所属类不同,sleep方法属于Thread类;wait属于Object类中,放在Object类中是因为Java中每个类都可以是一把锁。