Java的多執行緒(Java基礎複習歸納系列)
目錄
一、執行緒概述
1.程序
-
在一個作業系統中,每個獨立執行的程式都可稱之為一個程序,也就是“正在執行的程式”。
-
程序和程式
A process = a program in execution
一個程序應該包括:程式的程式碼;程式的資料;CPU暫存器的值,如PC,用來指示下一條將執行的指令、通用暫存器等;堆、棧;一組系統資源(如地址空間、開啟的檔案)
總之,程序包含了正在執行的一個程式的所有狀態資訊。
程序≠程式
- A program is C statements or commands----靜態的;
- A process is program + running context ----動態的.
- 目前大部分計算機上安裝的都是多工作業系統,即能夠同時執行多個應用程式。
- 在計算機中,所有的應用程式都是由CPU執行的,對於一個CPU而言,在某個時間點只能執行一個程式,也就是說只能執行一個程序。
2.執行緒
-
每個執行的程式都是一個程序,在一個程序中還可以有多個執行單元同時執行,這些執行單元可以看做程式執行的一條條路徑,被稱為執行緒。
-
作業系統中的每一個程序中都至少存在一個執行緒。當一個Java程式啟動時,就會產生了一個程序,該程序中會預設建立一個執行緒,在這個執行緒上會執行main()方法中的程式碼。
-
如果只是一個 cpu,它怎麼能夠同時執行多段程式呢?這是從巨集觀上來看的,cpu 一會執行 a 線索,一會執行 b 線索,切換時間很快,給人的感覺是 a,b 在同時執行(時間片輪轉)。
-
單執行緒和多執行緒程式
-
程式碼是按照呼叫順序依次往下執行,沒有出現兩段程式程式碼交替執行的效果,這樣的程式稱作單執行緒程式。
-
如果希望程式中實現多段程式程式碼交替執行的效果,則需要建立多個執行緒,即多執行緒程式。多執行緒程式在執行時,每個執行緒之間都是獨立的,它們可以併發執行。
-
-
執行緒與程序
- 程序是除CPU以外的資源分配的基本單位,執行緒是CPU的基本排程單位;
- 程序 = 執行緒 + 資源平臺。程序擁有一個完整的資源平臺,程序和程序之間不能共享資源。而執行緒共享所在程序的地址空間和其它資源,同時獨享必不可少的資源,如暫存器和棧;
- 執行緒同樣具有就緒、阻塞和執行三種基本狀態,同樣具有狀態之間的轉換關係;
- 執行緒 = 輕量級程序(lightweight process),執行緒相對於程序能減少併發執行的時間和空間開銷;
二、執行緒的建立
Java中提供了兩中建立新執行執行緒的方法:
1.Thread類
Thread類構造方法
Thread() |
構造一個新的執行緒物件 |
Thread(Runnable target) |
分配新的 Thread 物件,以便將 target 作為其執行物件。Target是一個實現Runnable介面的類的物件 |
Thread(String name) |
構造一個新的執行緒物件,並指定執行緒名 |
Thread(Runnable target,String name) |
分配新的 Thread 物件,以便將 target 作為其執行物件,將指定的 name 作為其名稱 |
常用方法:
static Thread currentThread() |
返回當前正在執行的執行緒物件 |
所有可能異常: InterruptedException - 如果任何執行緒中斷了當前執行緒。當丟擲該異常時,當前執行緒的中斷狀態被清除。 IllegalThreadStateException - 如果執行緒已經啟動。 SecurityException - 如果當前執行緒無法修改該執行緒 IllegalArgumentException - 如果優先順序不在 MIN_PRIORITY 到 MAX_PRIORITY 範圍內。 |
static void yield() |
使當前執行緒物件暫停,允許別的執行緒開始執行 |
|
void join() |
等待該執行緒終止。 |
|
void join(long millis) |
等待該執行緒終止的時間最長為 millis 毫秒。若millis 毫秒內被join的執行緒還沒有終止,則不再等待。 |
|
static void sleep(long millis) |
使當前執行緒暫停執行指定毫秒數 |
|
void start() |
啟動執行緒,加入就緒佇列 |
|
void run() |
Thread的子類應重寫此方法,內容應為該執行緒應執行的任務 |
|
void interrupt() |
中斷此執行緒 |
|
long getId() |
返回該執行緒的識別符號。 |
|
void setPriority(int newPriority) |
設定執行緒優先順序 |
|
void setDaemon(Boolean on) |
設定是否為後臺執行緒(on為 true時是)。該方法必須在啟動執行緒前呼叫 |
|
boolean isDaemon() |
測試該執行緒是否為守護執行緒。 |
|
boolean isAlive() |
判斷執行緒是否處於活動狀態 |
2.兩種建立多執行緒的方法
例: 假設售票廳有三個視窗可發售某日某次列車的100張車票,這時,100張車票可以看做共享資源,三個售票視窗需要建立三個執行緒分別顯示各個視窗的售票情況。
1)繼承Thread類建立多執行緒
- JDK中提供了一個執行緒類Thread,通過繼承Thread類,並重寫Thread類中的run()方法便可實現多執行緒。
- 在Thread類中,提供了一個start()方法用於啟動新執行緒,執行緒啟動後,系統會自動呼叫run()方法,如果子類重寫了該方法便會執行子類中的方法。
public class SellTickets1 {
public static void main(String[] args) {
TicketWindow t1 = new TicketWindow("視窗1");
TicketWindow t2 = new TicketWindow("視窗2");
TicketWindow t3 = new TicketWindow("視窗3");
t1.start();t2.start();t3.start();
}
}
class TicketWindow extends Thread {
int tickets = 100;
public TicketWindow(String name) {
this.setName(name);
}
@Override
public void run() {
while (tickets > 0) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + " 餘票: " + tickets--);
}
}
}
可以看出,每張票都被列印了三次。出現這樣現象的原因是在程式中建立了三個TicketWindow物件,就等於建立了三個售票程式,每個程式中都有100張票,每個執行緒在獨立地處理各自的資源,無法共享例項變數。
2)實現Runnable介面建立多執行緒
通過繼承Thread類實現了多執行緒,但是這種方式有一定的侷限性。因為Java中只支援單繼承,一個類一旦繼承了某個父類就無法再繼承Thread類
Thread類提供了另外一個構造方法Thread(Runnable target),其中Runnable是一個介面,它只有一個run()方法。當通過Thread(Runnable target))構造方法建立執行緒物件時,只需為該方法傳遞一個實現了Runnable介面的例項物件,這樣建立的執行緒將呼叫實現了Runnable介面中的run()方法作為執行程式碼,而不需要呼叫Thread類中的run()方法
例子中,為了保證資源共享,在程式中只能建立一個售票物件,然後開啟多個執行緒去執行這同一個售票物件的售票方法,簡單來說就是三個執行緒運行同一個售票程式,這時就需要用到多執行緒的第二種實現方式。
public class SellTickets2 {
public static void main(String[] args) {
TicketWindow t = new TicketWindow();
new Thread(t,"視窗1").start();// 使用構造方法Thread(Runnable target, String name)
new Thread(t,"視窗2").start();
new Thread(t,"視窗3").start();
}
}
class TicketWindow implements Runnable {
int tickets = 100;
@Override
public void run() {
while (tickets > 0) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + " 餘票: " + tickets--);
}
}
}
從上面可以看出通過繼承Thread類來獲得當前執行緒物件直接使用this即可;但通過實現Runnable介面來獲得當前執行緒物件,必須使用Thread.currentThread()方法。
3)兩種實現多執行緒方式的對比分析
實現Runnable介面相對於繼承Thread類來說,有如下顯著好處:
- 適合多個相同程式程式碼的執行緒去處理同一個資源的情況,把執行緒同程式程式碼、資料有效的分離,很好的體現了面向物件的設計思想。
- 可以避免由於Java的單繼承帶來的侷限性。在開發中經常碰到這樣一種情況,就是使用一個已經繼承了某一個類的子類建立執行緒,由於一個類不能同時有兩個父類,所以不能用繼承Thread類的方式,那麼就只能採用實現Runnable介面的方式。
3.後臺執行緒
對Java程式來說,只要還有一個前臺執行緒在執行,這個程序就不會結束,如果一個程序中只有後臺執行緒執行,這個程序就會結束。這裡提到的前臺執行緒和後臺執行緒是一種相對的概念,新建立的執行緒預設都是前臺執行緒,如果某個執行緒物件在啟動之前呼叫了setDaemon(true)語句,這個執行緒就變成一個後臺執行緒。
/**
* @Project laojiu
* @Title Daemon.java
* @Description TODO 測試後臺執行緒
* @Author 15643
* @Time 2018年8月16日 上午10:53:57
* @Other
*/
public class Daemon {
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread t = new Thread(new MyThreadDmn());
t.setDaemon(true);
t.start();
System.out.println("t是後臺執行緒嗎? "+t.isDaemon());
}
}
class MyThreadDmn implements Runnable {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
}
執行結果:
當開啟執行緒t後,會執行死迴圈中的列印語句,但實際情況是語句只打印幾次就結束了。這是因為我們將執行緒t設定為後臺執行緒後,當前臺執行緒死亡後,JVM會通知後臺執行緒死亡,由於後臺執行緒從接受指令到作出響應,需要一定的時間,因此,列印了幾次“Thread-0 is running.”語句後,後臺執行緒也結束了。由此說明程序中只有後臺執行緒執行時,程序就會結束。
三、執行緒的生命週期
執行緒整個生命週期可以分為五個階段,分別是新建狀態(New)、就緒狀態(Runnable)、執行狀態(Running)、阻塞狀態(Blocked)和死亡狀態(Terminated),執行緒的不同狀態表明瞭執行緒當前正在進行的活動。
1、新建狀態(New)
建立一個執行緒物件後,該執行緒物件就處於新建狀態,此時它不能執行,和其它Java物件一樣,僅僅由Java虛擬機器為其分配了記憶體,沒有表現出任何執行緒的動態特徵。
2、就緒狀態(Runnable)
當執行緒物件呼叫了start()方法後,該執行緒就進入就緒狀態(也稱可執行狀態)。處於就緒狀態的執行緒位於可執行池中,JVM會為其建立方法呼叫棧和程式計數器。此時它只是具備了執行的條件,能否獲得CPU的使用權開始執行,還需要等待JVM執行緒排程器的排程。
3、執行狀態(Running)
如果處於就緒狀態的執行緒獲得了CPU的使用權,開始執行run()方法中的執行緒執行體,則該執行緒處於執行狀態。如果計算機只有一個CPU,那麼在任何時刻只有一個執行緒處於執行狀態,若是一個多處理器的機器,將會有多個執行緒並行執行。當一個執行緒啟動後,它不可能一直處於執行狀態(除非它的執行緒執行體足夠短,瞬間就結束了),當使用完系統分配的時間後,系統就會剝奪該執行緒佔用的CPU資源,讓其它執行緒獲得執行的機會。需要注意的是,只有處於就緒狀態的執行緒才可能轉換到執行狀態。
併發性(concurrency):在同一時刻只能有一條指令在執行,但多個程序指令被快速輪換執行,使得在巨集觀上具有多個程序同時執行的效果。
並行性(parallel):在同一時刻,有多條指令在多個處理器上同時執行。
4、阻塞狀態(Blocked)
一個正在執行的執行緒在某些特殊情況下,如執行耗時的輸入/輸出操作時,會放棄CPU的使用權,進入阻塞狀態。執行緒進入阻塞狀態後,就不能進入排隊佇列。只有當引起阻塞的原因被消除後,執行緒才可以轉入就緒狀態。
-
當執行緒試圖獲取某個物件的同步鎖時,如果該鎖被其它執行緒所持有,則當前執行緒會進入阻塞狀態,如果想從阻塞狀態進入就緒狀態必須得獲取到其它執行緒所持有的鎖。
-
當執行緒呼叫了一個阻塞式的IO方法時,該執行緒就會進入阻塞狀態,如果想進入就緒狀態就必須要等到這個阻塞的IO方法返回。
-
當執行緒呼叫了某個物件的wait()方法時,也會使執行緒進入阻塞狀態,如果想進入就緒狀態就需要使用notify()方法喚醒該執行緒。
-
當執行緒呼叫了Thread的sleep(long millis)方法時,也會使執行緒進入阻塞狀態,在這種情況下,只需等到執行緒睡眠的時間到了以後,執行緒就會自動進入就緒狀態。
-
當在一個執行緒中呼叫了另一個執行緒的join()方法時,會使當前執行緒進入阻塞狀態,在這種情況下,需要等到新加入的執行緒執行結束後才會結束阻塞狀態,進入就緒狀態。
5、死亡狀態(Terminated)
執行緒的run()方法正常執行完畢或者執行緒丟擲一個未捕獲的異常(Exception)、錯誤(Error),執行緒就進入死亡狀態。一旦進入死亡狀態,執行緒將不再擁有執行的資格,也不能再轉換到其它狀態(如果對死亡狀態的執行緒再次呼叫start()方法,將會導致IllegalThreadStateException)。
四、執行緒排程與優先順序
1.執行緒的優先順序
-
優先順序越高的執行緒獲得CPU執行的機會越大,而優先順序越低的執行緒獲得CPU執行的機會越小。
-
執行緒的優先順序用1~10之間的整數來表示,數字越大優先順序越高。
-
除了可以直接使用數字表示執行緒的優先順序,還可以使用Thread類中提供的三個靜態常量表示執行緒的優先順序
|
執行緒可以具有的最低優先順序。相當於1 |
|
分配給執行緒的預設優先順序。相當於5 |
|
執行緒可以具有的最高優先順序。相當於10 |
程式在執行期間,處於就緒狀態的每個執行緒都有自己的優先順序,例如main執行緒具有普通優先順序。然而執行緒優先順序不是固定不變的,可以通過Thread類的setPriority(int newPriority)方法對其進行設定,該方法中的引數newPriority接收的是1~10之間的整數或者Thread類的三個靜態常量。也可以通過int getPriority()來獲取執行緒的優先順序。
2.執行緒休眠(sleep())
-
如果希望人為地控制執行緒,使正在執行的執行緒暫停,將CPU讓給別的執行緒,這時可以使用靜態方法sleep(long millis),該方法可以讓當前正在執行的執行緒暫停一段時間,進入休眠等待狀態。
-
當前執行緒呼叫sleep(long millis)方法後,在指定時間(引數millis)內該執行緒是不會執行的,這樣其它的執行緒就可以得到執行的機會了。
例:
import java.util.Date;
public class TestSleep {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new ThreadTs());
t.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "=======" + new Date());
Thread.sleep(1000);
}
}
}
class ThreadTs implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "===" + new Date());
try {
Thread.sleep(1100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
執行結果:
3.執行緒讓步(yield())
執行緒讓步可以通過yield()方法來實現,該方法和sleep()方法有點相似,都可以讓當前正在執行的執行緒暫停,但是它與sleep()有以下區別:
-
yield()方法不會阻塞該執行緒,它只是將執行緒轉換成就緒狀態,讓系統的排程器重新排程一次。而sleep()方法會將執行緒轉入阻塞狀態,只有達到阻塞時間才會轉入就緒狀態。
-
當某個執行緒呼叫yield()方法之後,只有與當前執行緒優先順序相同或者更高的執行緒才能獲得執行的機會。而sleep()方法不會理會其他執行緒的優先順序。
-
yield()方法沒有宣告丟擲任何異常,而sleep()方法宣告丟擲了InterruptedException。
public class YieldPriority {
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadYp(), "低優先順序執行緒");
Thread t2 = new Thread(new ThreadYp(), "高優先順序執行緒");
t1.setPriority(Thread.NORM_PRIORITY);
t2.setPriority(10);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "========> " + i);
if (i == 10) {
t1.start();
t2.start();
Thread.currentThread().yield();
}
}
}
}
class ThreadYp implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "=====> " + i);
}
}
}
4.執行緒插隊(join())
在Thread類中也提供了一個join()方法來實現這個“功能”。
當在某個執行緒中呼叫其它執行緒的join()方法時,呼叫的執行緒將被阻塞,直到被join()方法加入的執行緒執行完成後它才會繼續執行。
public class TestJoin {
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadJp(), "被join的執行緒,有時間限制");
Thread t2 = new Thread(new ThreadJp(), "被join的執行緒");
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "========> " + i);
if (i == 19) {
t1.start();
t2.start();
try {
t1.join(1);
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class ThreadJp implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "=====> " + i);
}
}
}
本例中,當i=20的時候,主執行緒會始終在t2執行完成之後執行,而t1可能在主執行緒之前執行也可能在其之後執行,主要是看其在1ms之內能否結束執行緒。
五、多執行緒同步
1.同步程式碼塊
在二、2.兩種建立多執行緒的方法中的兩個例子,執行後可能會出現如圖所示的“意外”情況:
剩餘的票變成了負數,這是由多執行緒操作共享資源tickets所導致的執行緒安全問題。
要想解決執行緒安全問題,必須得保證下面用於處理共享資源的程式碼在任何時刻只能有一個執行緒訪問。為了實現這種限制,Java中提供了同步機制。當多個執行緒使用同一個共享資源時,可以將處理共享資源的程式碼放置在一個程式碼塊中,使用synchronized關鍵字來修飾,被稱作同步程式碼塊,其語法格式如下:
synchronized(lock){
同步程式碼塊
}
lock是一個鎖物件,它是同步程式碼塊的關鍵。當執行緒執行同步程式碼塊時,首先會檢查鎖物件的標誌位,預設情況下標誌位為1,此時執行緒會執行同步程式碼塊,同時將鎖物件的標誌位置為0(加鎖)。當一個新的執行緒執行到這段同步程式碼塊時,由於鎖物件的標誌位為0,新執行緒會發生阻塞,等待當前執行緒執行完同步程式碼塊後(修改),鎖物件的標誌位被置為1(釋放鎖),新執行緒才能進入同步程式碼塊執行其中的程式碼。迴圈往復,直到共享資源被處理完為止。這種做法符合“加鎖-修改-釋放鎖”的邏輯,通過這種方式,保證併發執行緒在同一時刻只有一個執行緒可以進入修改共享資源的程式碼區(也稱為臨界區)。
將原來例子中的TicketWindow類做如下修改:
class TicketWindow implements Runnable {
int tickets = 100;
@Override
public void run() {
while (true) {
synchronized (this) {// 定義同步程式碼塊
if (tickets > 0) {
try {
Thread.sleep(30);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + " 餘票: " + tickets--);
}
}
}
}
}
執行結果:
2.同步方法
在方法前面同樣可以使用synchronized關鍵字來修飾,被修飾的方法為同步方法,它能實現和同步程式碼塊同樣的功能,具體語法格式如下:
synchronized 返回值型別 方法名([引數1,…]){
方法體
}
被synchronized修飾的方法在某一時刻只允許一個執行緒訪問,訪問該方法的其它執行緒都會發生阻塞,直到當前執行緒訪問完畢後,其它執行緒才有機會執行方法。
注意:
-
同步方法的lock鎖物件是this,也就是該物件本身。
-
synchronized關鍵字可以修飾方法、程式碼塊,但不能修飾構造器、屬性等。
public class SyncMethod {
public static void main(String[] args) {
Ticket t = new Ticket();
new Thread(t, "視窗1").start();
new Thread(t, "視窗2").start();
new Thread(t, "視窗3").start();
}
}
class Ticket implements Runnable {
int tickets = 100;
@Override
public void run() {
while (tickets > 0)
sale();
}
public synchronized void sale() {// 同步方法
if (tickets > 0) {
try {
Thread.sleep(30);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + " 餘票: " + tickets--);
}
}
}
六、多執行緒通訊
1.執行緒間通訊方法
如果想解決上述問題,就需要控制多個執行緒按照一定的順序輪流執行,此時需要讓執行緒間進行通訊。在Object類中提供了wait()、notify()、notifyAll()方法用於解決執行緒間的通訊問題,由於Java中所有類都是Object類的子類或間接子類,因此任何類的例項物件都可以直接使用這些方法。
wait() |
使當前執行緒放棄同步鎖並進入等待狀態, |
notify() |
喚醒一個在此同步鎖上處於等待狀態(呼叫wait())的執行緒,然後本執行緒繼續執行 |
notifyAll() |
喚醒在此同步鎖上所有處於等待狀態的執行緒,本執行緒繼續執行 |
但這三個方法必須由上了同步鎖的物件呼叫,可以分成以下兩種情況:
-
對於使用synchronized修飾的同步方法,因為該類的預設例項(this)就是同步鎖物件,所以可以在同步方法中直接呼叫這3個方法。
-
對於使用synchronized修飾的同步程式碼塊,同步鎖物件是synchronized後括號裡的物件,所以必須使用該物件呼叫這3個方法。
2.生產者消費者問題
設有一個緩衝區buffer,用一個數組來盛放資料。生產者執行緒(Producer)不斷產生資料,送buffer,消費者執行緒(Consumer)從buffer中取出資料列印。如不加控制,會出現多種列印結果,這取決於這兩個執行緒執行的相對速度。在這眾多的列印結果中,只有這兩個執行緒的執行剛好匹配的一種是正確的,其它均為錯誤。
public class ProducerConsumer{
public static void main(String[] args) {
Buffer b = new Buffer();
Thread t1 = new Thread(new Producer(b));
Thread t2 = new Thread(new Consumer(b));
t1.start();
t2.start();
}
}
class Buffer {
int[] data;// 盛放資料
int index = 0;// 記錄陣列中元素的下標
int id = 0;// 記錄陣列中元素的ID
public Buffer() {
data = new int[10];
}
// 生產方法
public synchronized void produce() {
data[index] = id;
System.out.println("input: " + index + ", " + data[index]);
index++;
id++;
while (index == data.length) {// 這裡使用while而不用if,防止catch到InterruptedException時無視條件,繼續往下執行
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
index = 0;// 操作完陣列的最後一個元素時索引置0,重新從陣列的第一個位置操作。
}
}
// 消費方法
public synchronized void consume() {
if (index > 0) {
index--;
System.out.println("output: " + index + ", " + data[index]);
}
while (index <= 0) {
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer b) {// 構造方法用來接收Buffer物件
buffer = b;
}
@Override
public void run() {
while (true) {
buffer.produce();
}
}
}
class Consumer implements Runnable {
private Buffer buffer;
public Consumer(Buffer b) {
buffer = b;
}
@Override
public void run() {
while (true) {
buffer.consume();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
執行結果:
本文部分內容來自於THU 諶衛軍老師教學PPT,特此感謝。