java.lang.Thread
位於java.lang包下的Thread是非常重要的線程類,它實現了Runnable接口。本篇文章圍繞Thread具體介紹一下Java線程的概念和Thread類中常用的方法
1.線程的狀態
線程從創建到最終的消亡, 要經歷若干個狀態。一般來說,線程包括以下幾個狀態:創建(new), 可運行(runnable), 阻塞(blocked), time waiting, waiting, 消亡(terminated)。
當需要建立起一個線程來執行某個子任務時,就創建了一個線程。但是線程創建之後,還沒有執行start()方法就不會開始執行. 另外因爲線程的運行需要一些條件(比如內存資源,程序計數器,Java虛擬機棧,這些都是線程私有的,要爲其分配一定的空間),只有程序運行需要的條件滿足了,才能進入可執行狀態。
當線程進入可執行狀態後,不代表立刻就能獲取CPU執行時間,也許CPU正在執行其他的線程,這時就需要等待。當得到CPU分配的執行時間後,線程才進入真正的運行狀態。
線程在運行過程中,可能有多個原因導致當前線程不繼續運行下去,比如用戶主動讓線程睡眠(睡眠一定時間後再重新執行),用戶主動讓線程等待,或者被同步塊阻塞,此時就對應着多個狀態:time waiting(睡眠或等待一定的時間),waiting(等待被喚醒),blocked(阻塞)。
線程遭到突然中斷或者子任務執行完畢,線程就會消亡。
2.上下文切換
對於單核CPU來說,CPU在一個時刻只能運行一個線程,當在運行一個線程的過程中轉去運行另外一個線程,這個叫做線程上下文切換(進程也類似)。
由於可能當前線程的任務並沒有執行完畢,所以在切換時需要保存線程的運行狀態,以便下此重新切換回來時能夠繼續切換之前的狀態運行。因此就需要從用戶態轉入內核態,由內核態去保存線程執行的現場(寄存器狀態,程序計數器等數據)。
所以對於線程上下文的切換其實就是 存儲和恢復CPU狀態的過程,它使得線程執行能夠從中斷點恢復執行。雖然多線程可以使得任務執行的效率得到提升。但是由於在線程切換時同樣會帶來一定的開銷代價,並且多個線程會導致系統資源佔用的增加,所以在進行多線程編程時要注意這些因素對效率的影響。
3.Thread類
Thread類實現了Runnable接口, 在Thread類中, 有一些比較關鍵的屬性, 比如name是表示當前線程的名字,priority代表當前線程的優先級(最大爲10,最小爲1,默認爲5),daemon表示線程是否是守護線程,target表示要執行的任務.
Thread類中常用的方法
1>start()方法
start()方法用來啓動一個線程, 當調用start方法後, 系統纔會開啓一個新的線程來執行用戶定義的子任務.在這個過程中, 會爲相應的線程分配需要的資源.
2>run()方法
run方法不需要讓用戶去調用, 而是讓用戶在建立線程時進行重寫. 當通過start方法啓動一個線程之後, 當線程獲得了CPU執行時間, 就會進入run方法體去執行具體的任務.
3>sleep方法
sleep相當於讓線程睡眠, 交出CPU, 讓CPU去執行其他的任務
如果需要讓當前的線程暫停一段時間,並進入阻塞狀態,則可以通過Thread類的靜態sleep()方法來實現
噹噹前線程調用sleep方法進入阻塞狀態後, 在其睡眠時間內, 該線程不會獲得執行機會. 即使系統中沒有其他可執行的線程, 處於sleep中的線程也不會執行, 因此sleep方法通常用來暫停程序的執行.
當sleep方法不會釋放鎖
public class SleepTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyTest());
Thread t2 = new Thread(new MyTest());
t1.start();
t2.start();
}
}
class MyTest implements Runnable {
@Override
public void run() {
synchronized (MyTest.class) {
System.out.println("線程:" + Thread.currentThread().getName() + "啓動");
try {
System.out.println("線程" + Thread.currentThread().getName() +"開始睡眠");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程:" + Thread.currentThread().getName() + "繼續");
}
}
}
在上面的例子中, 我們開啓兩個線程, 每個線程在同步代碼塊中都執行了sleep方法. 根據打印結果可以看出, 在第一個線程執行sleep方法之後, 第二個線程並沒有進入同步代碼塊, 也就是說沒有獲取到鎖. 所以sleep方法並不會讓當前的線程釋放鎖.
4>yield方法
yield方法與sleep方法有些類似, 也是Thread類提供的一個靜態方法, 也可以讓當前線程暫停. 但是yield方法不會阻塞當前線程, 它的作用是讓當前線程讓出自己的CPU執行時間, 轉入就緒狀態. 讓系統線程調度器重新調度一次. 所以完全有可能的是, 當某個線程調用了yield方法暫停之後, 線程調度器又將其調度出來重新執行.
與sleep方法相似, yield方法也不會釋放當前線程持有的鎖.
5>join方法
在main線程中, 調用thread.join方法, 則main線程會等待thread線程執行完畢或者等待一定的時間. 如果調用的是無參join方法, 則等待thread執行完畢, 如果調用的是指定了時間參數的join方法, 則等待一定時間.
public class JoinTest {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; ++i) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
});
t1.start();
try {
t1.join();
System.out.println("main結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上面的代碼中, main線程調用t1.join()方法之後, 在t1完成之後, 才繼續執行.
我們可以看一下join方法的源碼 :
join方法的實現本質上還是依靠wait方法,所以join方法會釋放當前線程的鎖. 其實質就是不斷判斷thread線程是否還存活, 如果存活就阻塞調用該方法的線程, 直到thread線程運行結束.
方法join擁有使線程排隊的作用, 有些類似於同步的運行效果.
6>interrupt方法
interrupt就是中斷的意思. 單獨調用interrupt方法可以使得處於阻塞狀態的線程拋出一個異常, 也就是說可以用來中斷一個處於阻塞狀態的線程; 另外, 通過interrupt方法和isInterrupted()方法可以來停止一個正在運行的線程
public class InterruptTest {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("線程開始休眠");
Thread.sleep(10000);
System.out.println("線程休眠結束");
} catch (InterruptedException e) {
System.out.println("線程被中斷");
}
System.out.println("線程執行結束");
}
});
t1.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行中斷");
t1.interrupt();
}
}
上述線程在執行sleep方法時處於阻塞狀態, 此時在main線程中調用了t1.interrupt()方法. t1線程拋出異常, 被中斷
但是interrupt方法並不能中斷正在運行的線程, 因爲調用interrupt方法相當於將中斷標誌位標記爲true, 可以在運行的線程中調用isInterrupted方法判斷中斷標誌是否被置位來中斷線程的執行. 另外 interrupted()方法可以檢測當前線程的interrupt狀態, 檢測完成後, 狀態清空
已經廢棄的方法
1>stop方法
stop方法是一個不安全的方法. 因爲調用stop方法會直接終止run方法的調用, 並且會拋出一個ThreadDeath錯誤, 如果線程持有哪個對象鎖的話, 會完全釋放鎖,導致對象狀態不一致.
2>destroy方法
已被廢棄
幾個關係到線程屬性的方法
1>getId
用來的到線程ID
2>getName和setName
用來得到或設置線程名字
3>getPriority和setPriority
用來獲取和設置線程優先級
4>setDaemon和isDaemon
用來設置線程是否成爲守護線程和判斷線程是否是守護線程.
守護線程和用戶線程的卻別是 : 守護線程依賴與創建它的線程, 而用戶線程不依賴. 例如如果在main線程中創建了一個守護線程, 當main方法執行完畢之後, 守護線程也會隨之消亡. 而用戶線程則不會, 用戶線程會一直運行到其運行完畢. 在JVM中,像垃圾收集器線程就是守護線程.