Java 线程状态流转图
一.线程状态流转图
Java的线程可以有多种状态,在Thread.State类中定义了6个常量来表示线程的状态,分别是NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED,下面是比较详细的一幅状态流转图:
二.示例代码
2.1 sleep
先看下面一段代码,测试sleep的时候是否会释放已经获取到的资源:
package cn.ganlixin.thread; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import java.util.concurrent.TimeUnit; @Slf4j public class TestSleep { // 创建一个对象,作为synchronized的monitor(锁) private static Object obj = new Object(); /** * 创建线程要完成的操作 */ public void work() { log.info("进入work,尝试获取monitor(obj)"); // 获取监视器(monitor) synchronized (obj) { log.info("获取到monitor"); try { // 进入的线程进行阻塞 TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 测试sleep操作是否会释放已经获取的资源 */ @Test public void test() throws InterruptedException { Runnable runnable = this::work; Thread t1 = new Thread(runnable); Thread t2 = new Thread(runnable); t1.start(); t2.start(); // 主线程阻塞等待t1和t2执行 TimeUnit.SECONDS.sleep(1); log.info("t1的状态:{}, t2的状态:{}", t1.getState(), t2.getState()); TimeUnit.SECONDS.sleep(100); } }
运行结果如下:
INFO 2020-06-16 18:08:14 [Thread-2] - 进入work,尝试获取monitor(obj) INFO 2020-06-16 18:08:14 [Thread-1] - 进入work,尝试获取monitor(obj) INFO 2020-06-16 18:08:14 [Thread-2] - 获取到monitor INFO 2020-06-16 18:08:15 [main] - t1的状态:BLOCKED, t2的状态:TIMED_WAITING INFO 2020-06-16 18:08:24 [Thread-1] - 获取到monitor
从输出结果可以可以看到,t1和t2同时启动,t2先进入同步块,获取到monitor后,t2继续执行,进行10秒sleep,休眠过程中t2处于TIMED_WAITING状态。
与此同时,由于t1未获取到monitor,只能阻塞等待t2释放monitor,此时t1处于BLOCKED状态;
等待10秒过后,t1 sleep结束,t1退出同步代码块,t2这才获取到monitor。
根据上面的例子总结一下,有以下几点:
1.线程进行sleep操作时,不会释放已经获取的资源;
2.线程进行sleep时,线程状态为TIMED_WAITING;
3.因为没有获取到monitor而发生线程阻塞,此时线程处于BLOCKED状态。
2.2 wait与notify
wait和后面的notify都是针对monitor来说的,可以进行线程间的通信。
下面介绍
package cn.ganlixin.thread; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import java.util.concurrent.TimeUnit; /** * 描述: * * @author ganlixin * @create 2020-06-16 */ @Slf4j public class TestWaitAndNotify { private static Object monitor = new Object(); @Test public void test() throws InterruptedException { Runnable doWaitRunnable = this::doWait; Runnable doNotifyRunnable = this::doNotify; // 两个线程执行相同的doWait,观察是否有线程阻塞 Thread t1 = new Thread(doWaitRunnable); Thread t2 = new Thread(doWaitRunnable); Thread t3 = new Thread(doNotifyRunnable); // t1和t2会进行wait t1.start(); t2.start(); // 休眠3秒,等待t1和t2运行起来 TimeUnit.SECONDS.sleep(3); log.info("t1的状态:{}, t2的状态:{}", t1.getState(), t2.getState()); // 启动t3,进行notify t3.start(); TimeUnit.SECONDS.sleep(10); } private void doWait() { log.info("doWait -> 进入到doWait"); synchronized (monitor) { log.info("doWait -> 获取到monitor"); try { // 阻塞 monitor.wait(); log.info("doWait -> wait状态解除"); } catch (InterruptedException e) { log.info("doWait -> 捕获InterruptedException,e=", e); } } } public void doNotify() { log.info("doNotify -> 进入doNotify"); synchronized (monitor) { log.info("doNotify -> 获取到monitor,开始notify"); // notify会随机选择一个调用了monitor.wait而等待线程 monitor.notify(); // notifyAll,会通知所有调用了monitor.wait的线程 // monitor.notifyAll(); log.info("doNotify -> notify完成"); } } }
运行上面的代码,运行结果如下:
INFO 2020-06-16 19:07:48 [Thread-2] - doWait -> 进入到doWait INFO 2020-06-16 19:07:48 [Thread-1] - doWait -> 进入到doWait INFO 2020-06-16 19:07:48 [Thread-1] - doWait -> 获取到monitor INFO 2020-06-16 19:07:48 [Thread-1] - doWait -> 获取到monitor INFO 2020-06-16 19:07:51 [main] - t1的状态:WAITING, t2的状态:WAITING INFO 2020-06-16 19:07:51 [Thread-3] - doNotify -> 进入doNotify INFO 2020-06-16 19:07:51 [Thread-3] - doNotify -> 获取到monitor,开始notify INFO 2020-06-16 19:07:51 [Thread-3] - doNotify -> notify完成 INFO 2020-06-16 19:07:51 [Thread-2] - doWait -> wait状态解除
从上面的运行结果可以看出,2个线程同时执行doWait,虽然有同步块synchronized(monitor),但是两个线程并没有发生阻塞,几乎同时获取到了monitor,获取到monitor后,t1和t2执行wait操作,此时两个线程的状态为WAITING状态;t3启动后,执行doNotify,同样能够获取都monitor,并进行notify操作,notify后,t2的wait状态接触。
根据这个例子,总结一下:
1.调用monitor的wait方法,会释放synchronized中的monitor,所以其他线程也可以获取到monitor,这点与sleep不同;
2.调用monitor.wait方法后,线程处于WAITING状态;
3.可以调用monitor.notify()或者monitor.notifyAll()来通知调用了monitor.wait()的线程,只不过notify()只会通知一个线程,并且这个随意选择一个线程通知,并不是按照wait顺序进行notify。
2.3suspend与resume
suspend挂起线程,resume让一个挂起的线程继续执行
package cn.ganlixin.thread; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import java.util.concurrent.TimeUnit; @Slf4j public class TestSuspendAndResume { private static Object monitor = new Object(); @Test public void test() throws InterruptedException { Runnable runnable = this::doWork; Thread t1 = new Thread(runnable); Thread t2 = new Thread(runnable); t1.start(); t2.start(); // 主线程休眠3秒,等待t1和t2执行 TimeUnit.SECONDS.sleep(3); log.info("t1状态:{}, t2状态:{}", t1.getState(), t2.getState()); log.info("开始resume t1"); t1.resume(); TimeUnit.SECONDS.sleep(1); log.info("t1状态:{}", t1.getState()); log.info("开始resume t2"); t2.resume(); TimeUnit.SECONDS.sleep(1); log.info("t2状态:{}", t2.getState()); TimeUnit.SECONDS.sleep(2); log.info("t1状态:{}, t2状态:{}", t1.getState(), t2.getState()); } public void doWork() { log.info("doWork -> 进入doWork,尝试获取monitor"); synchronized (monitor) { log.info("doWork -> 获取到monitor"); // suspend挂起线程,接口已经是deprecated Thread.currentThread().suspend(); log.info("doWork -> 挂起状态解除"); } } }
运行结果,这里运行了10次,发现会有不同的结果:
运行结果1如下
INFO 2020-06-16 19:52:47 [Thread-1] - doWork -> 进入doWork,尝试获取monitor INFO 2020-06-16 19:52:47 [Thread-2] - doWork -> 进入doWork,尝试获取monitor INFO 2020-06-16 19:52:47 [Thread-1] - doWork -> 获取到monitor INFO 2020-06-16 19:52:50 [main] - t1状态:RUNNABLE, t2状态:BLOCKED INFO 2020-06-16 19:52:50 [main] - 开始resume t1 INFO 2020-06-16 19:52:50 [Thread-1] - doWork -> 挂起状态解除 INFO 2020-06-16 19:52:50 [Thread-2] - doWork -> 获取到monitor INFO 2020-06-16 19:52:51 [main] - t1状态:TERMINATED INFO 2020-06-16 19:52:51 [main] - 开始resume t2 INFO 2020-06-16 19:52:51 [Thread-2] - doWork -> 挂起状态解除 INFO 2020-06-16 19:52:52 [main] - t2状态:TERMINATED INFO 2020-06-16 19:52:54 [main] - t1状态:TERMINATED, t2状态:TERMINATED
从这个结果中可以看出,t1和t2同时执行doWork,但是t1先获取到monitor,进入同步代码块,然后让t1调用suspend,将t1挂起,可以看到suspend让线程挂起的时候,并没有释放拥有的monitor,导致t2阻塞处于BLOCKED状态,而调用resume后的线程状态是RUNNABLE状态。
注意结果1中可以看到,resume的顺序和suspend的顺序相同,即t1 suspend后,再resume t1;t2 suspend后,再resume t2;而后面马上给出的运行结果2,就出现resume顺序和suspend顺序不相同的情况:
运行结果2如下:
INFO 2020-06-16 19:58:34 [Thread-2] - doWork -> 进入doWork,尝试获取monitor INFO 2020-06-16 19:58:34 [Thread-1] - doWork -> 进入doWork,尝试获取monitor INFO 2020-06-16 19:58:34 [Thread-2] - doWork -> 获取到monitor INFO 2020-06-16 19:58:37 [main] - t1状态:BLOCKED, t2状态:RUNNABLE INFO 2020-06-16 19:58:37 [main] - 开始resume t1 INFO 2020-06-16 19:58:38 [main] - t1状态:BLOCKED INFO 2020-06-16 19:58:38 [main] - 开始resume t2 INFO 2020-06-16 19:58:38 [Thread-2] - doWork -> 挂起状态解除 INFO 2020-06-16 19:58:38 [Thread-1] - doWork -> 获取到monitor INFO 2020-06-16 19:58:39 [main] - t2状态:TERMINATED INFO 2020-06-16 19:58:41 [main] - t1状态:RUNNABLE, t2状态:TERMINATED
从结果2中可以看出,
1.t1、t2进入doWork,t2先获取到monitor进入同步代码块,t1由于未获取到monitor处于BLOCKED状态,t2由于调用了suspend方法(挂起)而处于RUNNABLE状态;
2.先resume t1,但是t1的状态还是BLOCKED,然后resume t2,t2的挂起状态解除,t2释放monitor,t2完成任务后处于TERMINATED状态;
3.t1获取到monitor,t1执行suspend,一直到程序结束,t1都处于RUNNABLE状态,而不是预期的TERMINATED状态,这是因为resume t2的操作发生在 suspend t2之前。
根据这段代码,以及这两个运行结果,可以得出下面的结论:
1.调用suspend的线程不会释放已经拥有的资源;
2.调用suspend后,且未调用resume时,线程处于RUNNABLE状态(就绪);
3.resume可以发生在suspend之前,且不会抛出异常,只不过此时suspend线程将一直保持RUNNABLE状态。
4.一定要防止出现resume发生在suspend之前,其实suspend和resume已经被废弃了,所以不使用这两个方法就ok了。
2.4 join
join可以实现,等待一个线程结束后,再执行后面的操作,看下面的示例代码:
package cn.ganlixin.thread; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import java.util.concurrent.TimeUnit; @Slf4j public class TestJoin { @Test public void test() throws InterruptedException { Thread t1 = new Thread() { @Override public void run() { log.info("开始执行"); try { log.info("进行sleep 3秒"); TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } log.info("执行完毕"); } }; Thread t2 = new Thread() { @Override public void run() { log.info("开始执行"); try { log.info("等待t1完成"); t1.join(); log.info("已经等到t1完成工作,t2继续执行"); } catch (InterruptedException e) { e.printStackTrace(); } log.info("执行完毕"); } }; t1.start(); t2.start(); TimeUnit.SECONDS.sleep(1); log.info("t1状态:{}, t2状态:{}", t1.getState(), t2.getState()); TimeUnit.SECONDS.sleep(10); } }
运行结果如下:
INFO 2020-06-16 20:27:54 [Thread-1] - 开始执行 INFO 2020-06-16 20:27:54 [Thread-2] - 开始执行 INFO 2020-06-16 20:27:54 [Thread-2] - 等待t1完成 INFO 2020-06-16 20:27:54 [Thread-1] - 进行sleep 3秒 INFO 2020-06-16 20:27:55 [main] - t1状态:TIMED_WAITING, t2状态:WAITING INFO 2020-06-16 20:27:57 [Thread-1] - 执行完毕 INFO 2020-06-16 20:27:57 [Thread-2] - 已经等到t1完成工作,t2继续执行 INFO 2020-06-16 20:27:57 [Thread-2] - 执行完毕
从上面的运行结果得出以下结论:
1.调用t1.join()方法后,需要等待t1线程执行完毕后,t1.join()后面的语句才会执行;
2.t2在等待t1执行完毕的过程中,t2处于WAINTING状态。
2.5 yield
yield的功能:让当前线程“让”出cpu,注意,只是让出CPU,而不会释放已经获取到资源,看下面的示例即可:
package cn.ganlixin.thread; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import java.util.concurrent.TimeUnit; @Slf4j public class TestYield { private static Object monitor = new Object(); @Test public void test() throws InterruptedException { Runnable runnable = this::doWork; Thread t1 = new Thread(runnable); Thread t2 = new Thread(runnable); t1.start(); t2.start(); TimeUnit.SECONDS.sleep(2); log.info("t1状态:{}, t2状态:{}", t1.getState(), t2.getState()); // RUNNABLE TimeUnit.SECONDS.sleep(10); } public void doWork() { log.info("doWork -> 进入doWork"); synchronized (monitor) { log.info("doWork -> 获取到monitor,进行死循环yield"); while (true) { Thread.yield(); } } } }
运行代码,结果如下:
INFO 2020-06-16 20:50:48 [Thread-2] - doWork -> 进入doWork INFO 2020-06-16 20:50:48 [Thread-1] - doWork -> 进入doWork INFO 2020-06-16 20:50:48 [Thread-2] - doWork -> 获取到monitor,进行死循环yield INFO 2020-06-16 20:50:50 [main] - t1状态:BLOCKED, t2状态:RUNNABLE
可以看到,t1和t2同时进入doWork,但是t1先获取到monitor进入同步代码块,然后t1一直死循环进行yield,而yield的时候,并没有释放资源(monitor)。
所以,yield理论上会“礼让”CPU,但是礼让CPU的同时,并不会释放已经获取的资源。
2.6 park和unpark
park用于让当前线程等待(WAITING),unpark将处于等待的线程恢复正常。
package cn.ganlixin.thread; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; /** * 描述: * * @author ganlixin * @create 2020-06-16 */ @Slf4j public class TestParkAndUnpark { @Test public void test() throws InterruptedException { Thread t1 = new Thread() { @Override public void run() { log.info("开始park"); LockSupport.park(); log.info("WAITING状态解除"); } }; t1.start(); // 让t1先执行几秒 TimeUnit.SECONDS.sleep(2); log.info("t1的状态:{}", t1.getState()); log.info("开始执行unpark t1"); LockSupport.unpark(t1); TimeUnit.SECONDS.sleep(20); } }
输出结果如下:
INFO 2020-06-16 23:38:26 [Thread-1] - 开始park INFO 2020-06-16 23:38:28 [main] - t1的状态:WAITING INFO 2020-06-16 23:38:28 [main] - 开始执行unpark t1 INFO 2020-06-16 23:38:28 [Thread-1] - WAITING状态解除
从上面的运行结果可以看出,线程调用park后阻塞,线程进入WAITING状态,当调用unpark的时候,并没有抛出InterruptedException。