多執行緒程式設計學習一(Java多執行緒的基礎).
一、程序和執行緒的概念
程序:一次程式的執行稱為一個程序,每個 程序有獨立的程式碼和資料空間,程序間切換的開銷比較大,一個程序包含1—n個執行緒。程序是資源分享的最小單位。
執行緒:同一類執行緒共享程式碼和資料空間,每個執行緒有獨立的執行棧和程式計數器(PC),執行緒切換開銷小,執行緒是CPU排程的最小單位。
多程序:指作業系統能同時執行多個任務(程式)。
多執行緒:指同一個程式中有多個順序流在執行,執行緒是程序內部單一控制序列流。
執行緒和程序一樣包括:建立、就緒、執行、阻塞、銷燬 五個狀態:
1、新建狀態(New):新建立了一個執行緒物件。
2、就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,變得可執行,等待獲取CPU的使用權。
3、執行狀態(Running):就緒狀態的執行緒獲取了CPU,執行程式程式碼。
4、阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。阻塞的情況分三種:
(一)、等待阻塞:執行的執行緒執行wait()方法,JVM會把該執行緒放入等待池中。(wait會釋放持有的鎖)
(二)、同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池中。
(三)、其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)
5、死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。
二、多執行緒的優勢
單執行緒的特點就是排隊執行,也就是同步。而多執行緒能最大限度的利用CPU的空閒時間來處理其他的任務,系統的執行效率大大提升,使用多執行緒也就是在執行非同步。
三、使用多執行緒
實現多執行緒程式設計的方式主要有兩種,一種是繼承Thread類,另一種是實現Runable介面。其實,使用繼承Thread類的方式建立新執行緒時,最大的侷限就是不支援多繼承,因為Java語言的特點就是單根繼承,所以為了支援多繼承,完全可以實現Runnable介面,一邊實現一邊繼承。但是這兩種方式建立的執行緒在工作時的性質是一樣的,沒有本質的差別。
public class Thread1 extends Thread {
private int count=5;
@Override
public void run()
{
for (int i=0;i<2;i++){
System.out.println("現在是執行緒"+currentThread().getName()+"在執行:"+count--); }
}
}
public class Thread2 implements Runnable {
private int count=5;
@Override
public void run() {
for(int i=0;i<2;i++){
System.out.println("現在是執行緒"+Thread.currentThread().getName()+"在執行:"+count--); }
}
}
public class Test{
public static void main(String[] args){
//整合Thread類
Thread1 thread1=new Thread1();
Thread t1=new Thread(thread1,"A");
Thread t2=new Thread(thread1,"B");
Thread t3=new Thread(thread1,"C");
t1.start();
t2.start();
t3.start();
//實現Runable介面
Thread2 thread2=new Thread2();
Thread t4=new Thread(thread2,"A2");
Thread t5=new Thread(thread2,"B2");
Thread t6=new Thread(thread2,"C2");
t4.start();
t5.start();
t6.start();
}}
演示這個結果是為了說明以下下兩點:
1、CPU對執行緒的排程具有不確定性,採用“搶佔式”排程。
2、對於網上經常說的,實現Runnable介面的執行緒可以實現共享資料,而繼承Thread的執行緒就不能。其實不然,它們兩者的區別僅是單繼承的限制以及一些用法的不同(比如 如果你想對這個Thread物件做點別的事情(比如getName),那麼你就必須通過呼叫Thread.currentThread()方法得到對此執行緒的引用),沒有實質的差別。
多執行緒執行時為什麼呼叫的是start()方法而不是run()方法?
如果呼叫程式碼thread.run()就不是非同步執行了,而是同步,那麼此執行緒物件就不會交給“執行緒規劃器”來進行處理。而是由main主執行緒來呼叫run()方法,也就是說必須要等到run()方法中的程式碼執行完成後才可以執行後面的程式碼。start()用來啟動一個執行緒,當呼叫start()方法時,系統才會開啟一個執行緒,通過Thead類中start()方法來啟動的執行緒處於就緒狀態(可執行狀態),此時並沒有執行,一旦得到CPU時間片,就自動開始執行run()方法。
四、synchronized 關鍵字
多執行緒的鎖機制,通過在多執行緒要呼叫的方法前加入synchronized 關鍵字,使多個執行緒在執行方法時,要首先嚐試去拿這把鎖,如果能夠拿到這把鎖,那麼這個執行緒就可以執行synchronize裡面的程式碼。如果不能拿到這把鎖,那麼這個執行緒就會不斷地嘗試拿這把鎖,直到拿到這把鎖。synchronized 可以在任意物件及方法上加鎖,而加鎖的這段程式碼稱為“互斥區” 或 “臨界區”。
使用synchronized關鍵字主要是為了保證當前執行緒在執行過程中,不被其他執行緒搶佔並修改了共享的資源,從而導致執行緒不安全的情況出現。
五、常用執行緒方法
1、Thread.currentThread()方法:返回程式碼段正在被哪個執行緒呼叫的資訊。最常見的就是Thread.currentThread().getName()。
2、isAlive()方法:判斷當前的執行緒是否處於活動狀態。什麼是活動狀態呢?活動狀態就是執行緒已經啟動且尚未終止。執行緒正在執行或準備開始執行的狀態,就認為執行緒是“存活”的。
3、Thread.sleep()方法:在指定的毫秒數內讓"正在執行的執行緒"休眠(暫停執行)。這個“正在執行的執行緒”是指this.currentThread()返回的執行緒。
4、getId()方法:取得該執行緒的唯一標識。
5、Thread.interrupt()方法:用於中斷執行緒,這裡需要注意Thread.interrupt 的作用其實也不是中斷執行緒,而是「通知執行緒應該中斷了」,具體到底中斷還是繼續執行,應該由被通知的執行緒自己處理。具體來說,當對一個執行緒,呼叫 interrupt() 時,
① 如果執行緒處於被阻塞狀態(例如處於sleep, wait, join 等狀態),那麼執行緒將立即退出被阻塞狀態,並丟擲一個InterruptedException異常,並且清除中斷標誌,使之變為false。 ② 如果執行緒處於正常活動狀態,那麼會將該執行緒的中斷標誌設定為 true,僅此而已。被設定中斷標誌的執行緒將繼續正常執行,不受影響。
Thread thread = new Thread(() -> {
while (!Thread.interrupted()) {//通過這樣來檢查這個中斷標誌位是否設定為true,是否進行程式邏輯,請不要使用廢棄的Thread.stop, Thread.suspend, Thread.resume
// do more work.
}
});
thread.start();
// 一段時間以後
thread.interrupt();
值得一提的是,判斷執行緒是否中斷有兩個辦法:
- interrupted():測試當前執行緒是否已經是中斷狀態,執行後具有將狀態標誌清除的false的功能。(這裡需要特別注意的是即使是MyThread.interrupted(),測試的仍然是當前執行緒(this.currentThread())的狀態)。
- isInterrupted():測試執行緒Thread物件是否已經是中斷狀態,但不清除狀態標誌。
通過丟擲異常來中斷執行緒:
public class MyThread extends Thread {
@Override
public void run(){
try {
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("已經是停止狀態了!我要退出了");
throw new InterruptedException();
}
System.out.println("i=" + (i + 1));
}
} catch (InterruptedException e) {
System.out.println("進入MyThread.java類run方法中的catch了!");
e.printStackTrace();
}
}
}
另外,還可以通過retuen的方法來中斷執行緒。不過還是建議"拋異常"的方法來實現執行緒的停止,因為在catch塊中還可以將異常向上拋,使執行緒停止的事件得以傳播。
6、Thread.yield()方法:放棄當前的CPU資源,將它讓給其他的任務去佔用CPU的執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得CPU時間片。
7、setPriority()方法:為了程式的可移植性,建議植使用 MAX_PRIORITY , NORM_PRIORITY , MIN_PRIORITY 三個級別。設定優先順序並不意味著優先順序低的就得不到呼叫,只是CPU更傾向於讓高的優先順序先執行,但是CPU具體呼叫那個執行緒是無法確定的,設定優先順序只能保證說這個執行緒被呼叫的頻率比較高。
8、setDaemon(true):守護執行緒。守護執行緒是一個特殊的執行緒,它的特性有“陪伴”的含義,當程序中不存在非守護執行緒了,則守護執行緒自動銷燬。典型的守護執行緒就是垃圾回收執行緒,當程序中沒有非守護執行緒了,則垃圾回收執行緒也就沒有存在的必要了。
9、join()方法:等待該執行緒終止。join() 方法主要是讓呼叫該方法的thread完成run方法裡面的東西后, 再執行join()方法後面的程式碼,對join()方法的呼叫可以被中斷,做法是呼叫執行緒上的的interrupt()方法。
六、其他
1、stop()方法作廢:如果強制讓執行緒停止則有可能使一些清理性的工作得不到完成。另外一種情況就是對鎖定的物件進行了“解鎖”,導致資料得不到同步的處理,出現數據不一致的問題。
2、suspend()方法暫停執行緒。resume()方法恢復執行緒的執行。
3、在使用suspend()和resume()時,如果使用不當,極易造成公共的同步物件的獨佔,使得其他執行緒無法訪問公共同步物件。比如因為執行緒的暫停而導致資料不同步、suspend 和 resume會對訪問資源的鎖進行獨佔(i++沒有鎖、println()具有同步鎖)。