線程的五種狀態 線程從創建到銷毀一般分為五種狀態,如下圖: 1) 新建 當用new關鍵字創建一個線程時,就是新建狀態。 2) 就緒 調用了 start 方法之後,線程就進入了就緒階段。此時,線程不會立即執行run方法,需要等待獲取CPU資源。 3) 運行 當線程獲得CPU時間片後,就會進入運行狀態, ...
線程的五種狀態
線程從創建到銷毀一般分為五種狀態,如下圖:
1) 新建
當用new關鍵字創建一個線程時,就是新建狀態。
2) 就緒
調用了 start 方法之後,線程就進入了就緒階段。此時,線程不會立即執行run方法,需要等待獲取CPU資源。
3) 運行
當線程獲得CPU時間片後,就會進入運行狀態,開始執行run方法。
4) 阻塞
當遇到以下幾種情況,線程會從運行狀態進入到阻塞狀態。
- 調用sleep方法,使線程睡眠。
- 調用wait方法,使線程進入等待。
- 當線程去獲取同步鎖的時候,鎖正在被其他線程持有。
- 調用阻塞式IO方法時會導致線程阻塞。
- 調用suspend方法,掛起線程,也會造成阻塞。
需要註意的是,阻塞狀態只能進入就緒狀態,不能直接進入運行狀態。因為,從就緒狀態到運行狀態的切換是不受線程自己控制的,而是由線程調度器所決定。只有當線程獲得了CPU時間片之後,才會進入運行狀態。
5) 死亡
當run方法正常執行結束時,或者由於某種原因拋出異常都會使線程進入死亡狀態。另外,直接調用stop方法也會停止線程。但是,此方法已經被棄用,不推薦使用。
線程常用方法
1)sleep
當調用 Thread.sleep(long millis) 睡眠方法時,就會使當前線程進入阻塞狀態。millis參數指定了線程睡眠的時間,單位是毫秒。 當時間結束之後,線程會重新進入就緒狀態。
註意,如果當前線程獲得了一把同步鎖,則 sleep方法阻塞期間,是不會釋放鎖的。
2) wait、notify和notifyAll
首先,它們都是Object類中的方法。需要配合 Synchronized關鍵字來使用。
調用線程的wait方法會使當前線程等待,直到其它線程調用此對象的notify/notifyAll方法。 如果,當前對象鎖有N個線程在等待,則notify方法會隨機喚醒其中一個線程,而notifyAll會喚醒對象鎖中所有的線程。需要註意,喚醒時,不會立馬釋放鎖,只有當前線程執行完之後,才會把鎖釋放。
另外,wait方法和sleep方法不同之處,在於sleep方法不會釋放鎖,而wait方法會釋放鎖。wait、notify的使用如下:
public class WaitTest {
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
ListAdd listAdd = new ListAdd();
Thread t1 = new Thread(() -> {
synchronized (obj){
try {
for (int i = 0; i < 10; i++) {
listAdd.add();
System.out.println("當前線程:"+Thread.currentThread().getName()+"添加了一個元素");
Thread.sleep(300);
if(listAdd.getSize() == 5){
System.out.println("發出通知");
obj.notify();
}
}
} catch(InterruptedException e){
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (obj){
try {
if(listAdd.getSize() != 5){
obj.wait();
}
} catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("線程:"+Thread.currentThread().getName()+"被通知.");
}
});
t2.start();
Thread.sleep(1000);
t1.start();
}
}
class ListAdd {
private static volatile List<String> list = new ArrayList<String>();
public void add() {
list.add("abc");
}
public int getSize() {
return list.size();
}
}
以上,就是創建一個t2線程,判斷list長度是否為5,不是的話,就一直阻塞。然後,另外一個t1線程不停的向list中添加元素,當元素長度為5的時候,就去喚醒阻塞中的t2線程。
然而,我們會發現,此時的t1線程會繼續往下執行。直到方法執行完畢,才會把鎖釋放。t1線程去喚醒t2的時候,只是讓t2具有參與鎖競爭的資格。只有t2真正獲得了鎖之後才會繼續往下執行。
3) join
當線程調用另外一個線程的join方法時,當前線程就會進入阻塞狀態。直到另外一個線程執行完畢,當前線程才會由阻塞狀態轉為就緒狀態。
或許,你在面試中,會被問到,怎麼才能保證t1,t2,t3線程按順序執行呢。(因為,我們知道,正常情況下,調用start方法之後,是不能控制線程的執行順序的)
咳咳,當前青澀的我,面試時就被問到這個問題,是一臉懵逼。其實,是非常簡單的,用join方法就可以輕鬆實現:
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MultiT("a"));
Thread t2 = new Thread(new MultiT("b"));
Thread t3 = new Thread(new MultiT("c"));
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
}
}
class MultiT implements Runnable{
private String s;
private int i;
public MultiT(String s){
this.s = s;
}
@Override
public void run() {
while(i<10){
System.out.println(s+"===="+i++);
}
}
}
最終,我們會看到,線程會按照t1,t2,t3順序執行。因為,主線程main總會等調用join方法的那個線程執行完之後,才會往下執行。
4) yield
Thread.yield 方法會使當前線程放棄CPU時間片,把執行機會讓給相同或更高優先順序的線程(yield英文意思就是屈服,放棄的意思嘛,可以理解為當前線程暫時屈服於別人了)。
註意,此時當前線程不會阻塞,只是進入了就緒狀態,隨時可以再次獲得CPU時間片,從而進入運行狀態。也就是說,其實yield方法,並不能保證,其它相同或更高優先順序的線程一定會獲得執行權,也有可能,再次被當前線程拿到執行權。
yield方法和sleep方法一樣,也是不釋放鎖資源的。可以通過代碼來驗證這一點:
public class TestYield {
public static void main(String[] args) {
YieldThread yieldThread = new YieldThread();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(yieldThread);
t.start();
}
}
}
class YieldThread implements Runnable {
private int count = 0;
@Override
public synchronized void run() {
for (int i = 0; i < 10; i++) {
count ++;
if(count == 1){
Thread.yield();
System.out.println("線程:"+Thread.currentThread().getName() + "讓步");
}
System.out.println("線程:"+Thread.currentThread().getName() + ",count:"+count);
}
}
}
結果:
會看到,線程讓步之後,並不會釋放鎖。因此,其它線程也沒機會獲得鎖,只能把當前線程執行完之後,才會釋放。(對於這一點,其實我是有疑問的。既然yield不釋放鎖,那為什麼還要放棄執行權呢。就算放棄了執行權,別的線程也無法獲得鎖啊。)
所以,我的理解,yield一般用於不存在鎖競爭的多線程環境中。如果當前線程執行的任務時間可能比較長,就可以選擇用yield方法,暫時讓出CPU執行權。讓其它線程也有機會執行任務,而不至於讓CPU資源一直消耗在當前線程。
5)suspend、resume
suspend 會使線程掛起,並且不會自動恢復,只有調用 resume 方法才能使線程進入就緒狀態。註意,這兩個方法由於有可能導致死鎖,已經被廢棄。