一.線程狀態流轉圖
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。