1. 程式人生 > >多線程知識體系

多線程知識體系

join方法 同時 exe stack 技術 cpu 退出 lock 對象創建

一、線程與進程
進程:進程往往被看成一個應用或者程序的代名詞,它擁有自己的內存空間。每個進程擁有自己的獨立代碼和數據空間。進程的切換開銷大,一個進程包括1-n個線程。
線程:線程有時被看作為輕量級的進程,線程共享代碼和內存空間。線程共享進程的資源,如打開的資源,這也導致了線程間通信的不安全性。

二、實現多線程
實現java多線程有三種方式:繼承thread類,實現runnable接口,使用ExecutorService、Callable、Future實現由返回結果的多線程。
註意:啟動一個線程,是調用該線程的start()方法,而不是直接調用run()方法;調用了start()方法後,並不會立即執行線程代碼,而是將該線程變為可執行狀態(runnable),線程什麽時候執行是由操作系統決定的。

三、繼承thread和實現runnable的區別
1):適合多個相同的程序代碼的線程去處理同一個資源
2):可以避免java中的單繼承的限制
3):增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨立

註意:

  繼承thread直接new就可以得到多個線程;
  對於實現runnable的接口的類,只能new一次,然後以這個實例化的類new出線程。
  詳細見示例代碼。
  java代碼運行至少啟動了兩個線程,一個main線程,一個垃圾收集線程。當java命令執行一個類時,實際上會啟動jvm,每個jvm是在操作系統中啟動了一個進程。

四、線程狀態轉換

技術分享

1、新建狀態(New):新創建了一個線程對象。
2、就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。
3、運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。(註意:這個狀態由系統分配,操作系統決定什麽時候開始運行這個線程。
4、阻塞狀態(Blocked):阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。
阻塞的情況分三種:
(一)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。
(二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池中。
(三)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
5、死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。

五、線程調度
Thread類的setPriority()和getPriority()方法分別用來設置和獲取線程的優先級。
  static int MAX_PRIORITY
  線程可以具有的最高優先級,取值為10。
  static int MIN_PRIORITY
  線程可以具有的最低優先級,取值為1。
  static int NORM_PRIORITY
  分配給線程的默認優先級,取值為5。

  線程睡眠:Thread.sleep(long millis)方法,使線程轉到阻塞狀態。millis參數設定睡眠的時間,以毫秒為單位。當睡眠結束後,就轉為就緒(Runnable)狀態。sleep()平臺移植性好。
  線程等待:Object類中的wait()方法,導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價於調用 wait(0) 一樣。
  線程讓步:Thread.yield() 方法,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。
  線程加入:join()方法,等待其他線程終止。在當前線程中調用另一個線程的join()方法,則當前線程轉入阻塞狀態,直到另一個進程運行結束,當前線程再由阻塞轉為就緒狀態。
  線程喚醒:Object類中的notify()方法,喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現做出決定時發生。線程通過調用其中一個 wait 方法,在對象的監視器上等待。 直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭;例如,喚醒的線程在作為鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。類似的方法還有一個notifyAll(),喚醒在此對象監視器上等待的所有線程。

六、常用函數說明
1、join():指等待t線程終止;
  例如:主線程中調用一個很耗時的線程,可能主線程執行完了,調用的線程還沒執行完,但主線程又需要被調用線程的結果。因此就可以在主線程中執行調用線程的join方法,等待調用線程終止。
2、yield():暫停當前正在執行的線程對象,並執行其他線程;
  Thread.yield()方法作用是:暫停當前正在執行的線程對象,並執行其他線程。
yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調度程序再次選中。

註意:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。
3、sleep和yield方法區別;
  sleep方法使線程進入阻塞狀態,在sleep的時間段內,線程不會被執行;yield方法使線程變成可執行狀態,可以會被系統立刻調用執行了。
sleep 方法使當前運行中的線程睡眼一段時間,進入不可運行狀態,這段時間的長短是由程序設定的,yield 方法使當前線程讓出 CPU 占有權,但讓出的時間是不可設定的。實際上,yield()方法對應了如下操作:先檢測當前是否有相同優先級的線程處於同可運行狀態,如有,則把 CPU 的占有權交給此線程,否則,繼續運行原來的線程。所以yield()方法稱為“退讓”,它把運行機會讓給了同等優先級的其他線程。
另外,sleep 方法允許較低優先級的線程獲得運行機會,但 yield() 方法執行時,當前線程仍處在可運行狀態,所以,不可能讓出較低優先級的線程些時獲得 CPU 占有權。在一個運行系統中,如果較高優先級的線程沒有調用 sleep 方法,又沒有受到 I\O 阻塞,那麽,較低優先級線程只能等待所有較高優先級的線程運行結束,才有機會運行。
4、interrupt();
  中斷某個線程,這種結束方式比較粗暴,如果t線程打開了某個資源還沒來得及關閉也就是run方法還沒有執行完就強制結束線程,會導致資源無法關閉。
  要想結束進程最好的辦法就是用sleep()函數的例子程序裏那樣,在線程類裏面用以個boolean型變量來控制run()方法什麽時候結束,run()方法一結束,該線程也就結束了。
5、wait(),notify();
  Obj.wait(),與Obj.notify()必須要與synchronized(Obj)一起使用,也就是wait,與notify是針對已經獲取了Obj鎖進行操作,從語法角度來說就是Obj.wait(),Obj.notify必須在synchronized(Obj){...}語句塊內。從功能上來說wait就是說線程在獲取對象鎖後,主動釋放對象鎖,同時本線程休眠。直到有其它線程調用對象的notify()喚醒該線程,才能繼續獲取對象鎖,並繼續執行。
  相應的notify()就是對對象鎖的喚醒操作。但有一點需要註意的是notify()調用後,並不是馬上就釋放對象鎖的,而是在相應的synchronized(){}語句塊執行結束,自動釋放鎖後,JVM會在wait()對象鎖的線程中隨機選取一線程,賦予其對象鎖,喚醒線程,繼續執行。這樣就提供了在線程間同步、喚醒的操作。
Thread.sleep()與Object.wait()二者都可以暫停當前線程,釋放CPU控制權,主要的區別在於Object.wait()在釋放CPU同時,釋放了對象鎖的控制。

例子: 建立三個線程,A線程打印10次A,B線程打印10次B,C線程打印10次C,要求線程同時運行,交替打印10次ABC。這個問題用Object的wait(),notify()就可以很方便的解決。

public class ThreadABC implements Runnable {

private String name;
private Object prev;
private Object self;

private ThreadABC(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}

@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;

self.notify();
}
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
}

public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
ThreadABC pa = new ThreadABC("A", c, a);
ThreadABC pb = new ThreadABC("B", a, b);
ThreadABC pc = new ThreadABC("C", b, c);

new Thread(pa).start();
Thread.sleep(100); // 確保按順序A、B、C執行
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
}

輸出結果:
ABCABCABCABCABCABCABCABCABCABC

  先來解釋一下其整體思路,從大的方向上來講,該問題為三線程間的同步喚醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循環執行三個線程。為了控制線程執行的順序,那麽就必須要確定喚醒、等待的順序,所以每一個線程必須同時持有兩個對象鎖,才能繼續執行。一個對象鎖是prev,就是前一個線程所持有的對象鎖。還有一個就是自身對象鎖。主要的思想就是,為了控制執行的順序,必須要先持有prev鎖,也就前一個線程要釋放自身對象鎖,再去申請自身對象鎖,兩者兼備時打印,之後首先調用self.notify()釋放自身對象鎖,喚醒下一個等待線程,再調用prev.wait()釋放prev對象鎖,終止當前線程,等待循環結束後再次被喚醒。運行上述代碼,可以發現三個線程循環打印ABC,共10次。程序運行的主要過程就是A線程最先運行,持有C,A對象鎖,後釋放A,C鎖,喚醒B。線程B等待A鎖,再申請B鎖,後打印B,再釋放B,A鎖,喚醒C,線程C等待B鎖,再申請C鎖,後打印C,再釋放C,B鎖,喚醒A。看起來似乎沒什麽問題,但如果你仔細想一下,就會發現有問題,就是初始條件,三個線程按照A,B,C的順序來啟動,按照前面的思考,A喚醒B,B喚醒C,C再喚醒A。但是這種假設依賴於JVM中線程調度、執行的順序,故在main方法中讓ABC線程以此初始化。

6、wait和sleep的區別
共同點:
1. 他們都是在多線程的環境下,都可以在程序的調用處阻塞指定的毫秒數,並返回。
2. wait()和sleep()都可以通過interrupt()方法 打斷線程的暫停狀態 ,從而使線程立刻拋出InterruptedException。
如果線程A希望立即結束線程B,則可以對線程B對應的Thread實例調用interrupt方法。如果此刻線程B正在wait/sleep /join,則線程B會立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結束線程。
需要註意的是,InterruptedException是線程自己從內部拋出的,並不是interrupt()方法拋出的。對某一線程調用 interrupt()時,如果該線程正在執行普通的代碼,那麽該線程根本就不會拋出InterruptedException。但是,一旦該線程進入到 wait()/sleep()/join()後,就會立刻拋出InterruptedException 。
不同點:
1. Thread類的方法:sleep(),yield()等
Object的方法:wait()和notify()等
2. 每個對象都有一個鎖來控制同步訪問。Synchronized關鍵字可以和對象的鎖交互,來實現線程的同步。sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。
3. wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep可以在任何地方使用
4. sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常。

七、常見線程名詞解釋
1、主線程:JVM調用程序main()所產生的線程。
2、當前線程:這個是容易混淆的概念。一般指通過Thread.currentThread()來獲取的進程。
3、後臺線程:指為其他線程提供服務的線程,也稱為守護線程。JVM的垃圾回收線程就是一個後臺線程。用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束
4、前臺線程:是指接受後臺線程服務的線程,其實前臺後臺線程是聯系在一起,就像傀儡和幕後操縱者一樣的關系。傀儡是前臺線程、幕後操縱者是後臺線程。由前臺線程創建的線程默認也是前臺線程。可以通過isDaemon()和setDaemon()方法來判斷和設置一個線程是否為後臺線程。

sleep(): 強迫一個線程睡眠N毫秒。
isAlive(): 判斷一個線程是否存活。
join(): 等待線程終止。
activeCount(): 程序中活躍的線程數。
enumerate(): 枚舉程序中的線程。
currentThread(): 得到當前線程。
isDaemon(): 一個線程是否為守護線程。
setDaemon(): 設置一個線程為守護線程。
setName(): 為線程設置一個名稱。
wait(): 強迫一個線程等待。
notify(): 通知一個線程繼續運行。
setPriority(): 設置一個線程的優先級。

八、線程同步
九、線程數據傳遞
9.1、通過構造方法傳遞數據
9.2、通過變量和方法傳遞數據
set方法傳遞。
9.3、通過回調函數傳遞數據
見例子。

多線程知識體系