Java多線程,java多線程執行個體
Windows等作業系統均支援多線程進程的並發處理機制。作業系統支援多線程,使多個程式能夠並發執行,以改善資源使用率和提高系統效率;作業系統支援多線程,能夠減少程式並發時所付出的時間和空間開銷,使得開發粒度更細,並發性更好。
進程
進程是一個程式關於某個資料集合的一次執行過程,是作業系統進行資源分派和保護的基本單位。進程具有以下特性:
①結構性。進程包含了資料集合和運行於其上的程式。每個進程至少由三要素組成:程式塊、資料區塊和進程式控制制塊。進程式控制制塊(Process Control Block, PCB)描述和記錄進程的動態變化過程,使進程能正確運行。
②獨立性。進程既是系統中資源分派和保護的基本單位,也是系統調度的獨立單位(單線程進程),每個進程都以各自獨立的速度在CPU上運行。
③動態性。進程是程式在資料集合上的一次執行過程,是動態概念。它的生命週期在多個狀態間變化,由建立而產生,由調度而執行,因等待條件而阻塞,由撤銷而消亡。程式是一組有序指令序列,是靜態概念,程式作為一種系統資源是永久存在的。
④並發性。進程的並發性是指一組進程的執行在時間上是重疊的。
⑤互動性。多個進程可以共用變數,通過共用變數實現互相通訊,多個進程之間能夠協作完成一個任務。
線程
線程是進程中能夠獨立執行的實體(控制流程),是處理器調度和分配的基本單位。線程是進程的組成部分,每個進程內允許包含多個並發執行的線程。同一個進程中的所有線程共用進程獲得的記憶體空間和資源,但不擁有資源。
支援多線程的進程成為多線程進程。
線程的主要特性如下:
①結構性。線程是作業系統調度的基本單位,具有唯一的標識符合線程式控制制塊,其中包含調度所需的一切資訊。
②動態性。線程是動態,而且有狀態變化。當建立一個進程時,同時至少為其建立一個線程,需要時再建立其他線程。終止一個進程將導致進程中的所有線程終止。
③並發性。同一進程的多個線程可在一個或多個處理器上並發或並行的執行,進程之間的並發執行演變為線程之間的並發執行。在單一處理器上,從宏觀上看,在一個時間段中有幾個線程都處於運行狀態;在微觀上看,任意時刻僅有一個線程在處理器上運行。並發的實質是一個處理器在多個線程之間的多工,是對有限的實體資源強制行使多使用者共用,消除電腦不見之間的互等現象,提高系統資源使用率。
④共用性。同一進程的所有線程共用但部擁有進程的狀態和資源,且駐留在進程的記憶體空間中,可以訪問相同的資料。所有線程之間需要有通訊和同步機制。
線程的狀態
線程在其生命週期中經曆著狀態的變化,線程狀態包括5種:建立、就緒、運行、阻塞、終止。
就緒(ready)態——進程具備運行條件,等待系統分配處理器以便運行。
運行(running)態——進場佔用處理器正在運行。
阻塞(blocked)態——進程不具備運行條件,正在等待某個事件的完成。
線程在執行過程中的任一時刻,處於一種狀態,根據運行條件在多個狀態之間轉變。一個進程建立後處於就緒態,運行中因等待條件處於阻塞態。
線程調度
任一時刻只有一個線程能夠佔用一個處理器運行,按照什麼原則決定就緒隊列中的哪個線程能夠獲得處理器就是線程調度的任務。
線程調度的功能就是按照某種原則選擇一個線程使它獲得處理器運行。線程調度是作業系統的核心部分,線程調度策略的優劣直接影響到作業系統的效能。
線程調度採用剝奪方式,當一個線程正在處理器上執行時,作業系統可以根據規定的原則剝奪它的處理器使用權,而把處理器分配給其他線程使用。常用的剝奪原則有兩種:一是高優先順序線程可以剝奪低優先順序線程運行;二是當運行線程時間使用完後被菠蘿處理器。
並發程式設計
順序程式設計方法是指,程式模組按照語句次序順序執行,其特性為:①執行的順序性;②運行環境的封閉性;③執行結果的確定性;④計算解僱偶的可再現性。
並發程式設計方法是指,將一個程式分為若干可同時執行的程式模組,每個程式模組和它執行時所處理的資料結合組成一個進程。作業系統以進程作為系統資源分派的基本單位,以線程系統調度的基本單位。其特性如下:
①並發執行的線程之間不具有順序性。線程由作業系統調度執行,不會按照語句的書寫順序執行。
②運行環境不再是封閉的,一個線程的執行可能影響其他線程的執行結果。(計算過程不可再現)
③共用變數的多個線程(成為互動線程)之間實現線程通訊,能夠協作完成一個任務,也會出現與時間有關的錯誤。
④並發多線程程式設計的優點是,提高了系統效能,具體表現為快速切換線程、減少系統管理開銷、線程通訊易於實現、並發按程式提高、節省記憶體空間。
Java的線程對象
Java支援內建的多線程機制。Java語言套件中的Runnable介面約定線程的執行方法,Thread類提供建立、管理和控制線程對象的方法。
在java中可有兩種方式實現多線程,一種是繼承Thread類,一種是實現Runnable介面;Thread類是在java.lang包中定義的。一個類只要繼承了Thread類同時覆寫了本類中的run()方法就可以實現多線程操作了,但是一個類只能繼承一個父類,這是此方法的局限。
public class NumberThread extends Thread{ private int first; public NumberThread(String name, int first){ super(name); this.first = first; } public void run(){ System.out.print(this.getName() + ":"); for (int i = first; i < 100; i += 2){ System.out.print(i + " "); } System.out.println(this.getName() + "End!\n"); } public static void main(String[] args){ System.out.println("Current Thread:" +Thread.currentThread().getName()); NumberThread thread1 = new NumberThread("JISHU", 1); NumberThread thread2 = new NumberThread("OUSHU", 2); thread1.start(); thread2.start(); System.out.println("Active Count:" + Thread.activeCount()); }}
在JDK的安裝路徑下,src.zip是全部的java來源程式,通過此代碼找到Thread中的start()方法的定義,可以發現此方法中使用了private native void start0();其中native關鍵字表示可以叫用作業系統的底層函數,那麼這樣的技術成為JNI技術(java Native Interface)。
在實際開發中一個多線程的操作很少使用Thread類,而是通過Runnable介面完成。但是在使用Runnable定義的子類中沒有start()方法,只有Thread類中才有。此時觀察Thread類,有一個構造方法:public Thread(Runnable targer)此構造方法接受Runnable的子類執行個體,也就是說可以通過Thread類來啟動Runnable實現的多線程。(start()可以協調系統的資源)
public class NumberRunnable implements Runnable{ private int first; public NumberRunnable(int first){ this.first = first; } public void run(){ for (int i = first; i < 50; i += 2){ System.out.print(i + " "); } System.out.println("End!\n"); } public static void main(String[] args){ NumberRunnable target = new NumberRunnable(1); Thread thread1 = new Thread(target, "JISHU"); thread1.start(); new Thread(new NumberRunnable(2), "OUSHU").start(); }}
兩種實現方式的區別和聯絡:
在程式開發中只要是多線程肯定永遠以實現Runnable介面為主,因為實現Runnable介面相比繼承Thread類有如下好處:
①適合多個相同的程式碼的線程去處理同一個資源(適合於資源共用)
②可以避免java中的單繼承的限制
③增加程式的健壯性,代碼可以被多個線程共用,代碼和資料獨立。
用一個買票程式來說明兩種實現方式的區別:
繼承Thread類:
public class TicketThread extends Thread { private int count = 10; public TicketThread(String name){ super(name); } public void run(){ while (count > 0){ System.out.println(Thread.currentThread().getName() + " is selling ticket: " + count); count--; } } public static void main(String[] args){ TicketThread thread1 = new TicketThread("視窗1"); TicketThread thread2 = new TicketThread("視窗2"); thread1.start(); thread2.start(); }}
輸出結果為:
視窗2 is selling ticket: 10視窗2 is selling ticket: 9視窗2 is selling ticket: 8視窗2 is selling ticket: 7視窗2 is selling ticket: 6視窗2 is selling ticket: 5視窗2 is selling ticket: 4視窗2 is selling ticket: 3視窗2 is selling ticket: 2視窗2 is selling ticket: 1視窗1 is selling ticket: 10視窗1 is selling ticket: 9視窗1 is selling ticket: 8視窗1 is selling ticket: 7視窗1 is selling ticket: 6視窗1 is selling ticket: 5視窗1 is selling ticket: 4視窗1 is selling ticket: 3視窗1 is selling ticket: 2視窗1 is selling ticket: 1
實現Runnable介面:
public class TicketRunnable implements Runnable{ private int count = 10; public void run(){ while (count > 0){ System.out.println(Thread.currentThread().getName() + " is selling ticket: " + count--); } } public static void main(String[] args){ TicketRunnable target = new TicketRunnable(); new Thread(target, "視窗1").start(); new Thread(target, "視窗2").start(); }}
輸出結果為:
視窗1 is selling ticket: 9視窗1 is selling ticket: 8視窗1 is selling ticket: 7視窗1 is selling ticket: 6視窗1 is selling ticket: 5視窗1 is selling ticket: 4視窗1 is selling ticket: 3視窗1 is selling ticket: 2視窗1 is selling ticket: 1視窗2 is selling ticket: 10
如果多次執行,可以發現上述程式可能出現賣出編號為0的票的情況,這裡涉及線程的同步機制,將在後續文章中提到。
線程對象的優先順序
Java提供10個等級的線程優先順序,分別用1~10表示,優先順序最低為1,最高為10,預設值是5。Thread類聲明了以下三個表示優先順序的公有靜態常量:
public static final int MIN_PRIORITY = 1; //最低優先順序
public static final int MAX_PRIORITY = 2; //最高優先順序
public static final int NORM_PRIORITY = 3; //預設優先順序
每個線程兌現建立時自動獲得預設優先順序5,調用setPriority()方法可以改變線程對象的優先順序。
線程對象的生命週期
6種線程狀態:
①建立態(NEW)。已建立,未啟動。
②運行態(RUNNABLE)。從作業系統角度看,處於建立態的線程啟動後,進入就緒態,再由作業系統調度執行而成為運行態。由於線程調度由作業系統控制和管理,程式無法控制,無法區分就緒態和運行態。所以,從程式設計角度看,線程啟動後即進入運行態RUNNABLE。進入運行態的線程執行其run()方法。
③阻塞態(BLOCKED)和等待態。一個運行態的線程因某種原因不嫩繼續運行時,進入阻塞態或等待態。等待態有兩種:WAITING(等待時間不確定)和TIMED_WAITING(等待時間確定)。
④終止態(TERMINATED)。線程對象停止運行未被撤銷時是終止態。
Thread類中改變和判斷線程狀態的方法:
- start() 建立態到運行態。
- isAlive() 判斷線程是否為活動狀態。當一個線程未被終止時,返回true,此時線程處於運行態、阻塞態、等待態之一。但一個線程未啟動或已終止時,返回false。
- sleep() 方法使當前進程停止執行若干毫秒,線程由運行態進行等待態,睡眠時間到,線程可再次進行運行態。
- interrupt()方法為當前線程設定一個中斷標記,以便於run()方法運行時使用IsInterrupted()能夠檢測到。此時,線程愛sleep()之類的方法中被阻塞時,由sleep()方法拋出java.lang.InterruptedException,線程中斷異常,可捕獲這個異常進行中斷處理操作。
interrupt()只是為線程設定一個中斷標記,並沒有中斷線程運行,該方法沒有拋出異常。一個線程被設定了中斷標記後仍可運行,isAlive()返回true。
當拋出一個InterruptedException異常時,記錄該線程中斷情況的標記將會被清除,這樣再調用isInterrupted()將返回false。
線程的同步機制
互動線程間存在兩種關係:競爭關係和協作關係。
對於競爭關係的互動線程間需要採用線程互斥方式解決共用資源衝突問題;對於協作關係的互動線程間需要採用線程同步方式解決線程間通訊及因執行速度不一致而引起的不同步問題。
互動的並發線程是指他們共用某些變數,一個線程的執行可能影響到其他線程的執行結果,互動的並發線程之間具有制約關係。
無關線程間並發執行時,不會產生於時間有關的錯誤。
資源競爭出現了兩個問題:一個是死結(deadlock)問題,一組線程如果都獲得了部分資源,還想得到其他線程所佔用的資源,最終所有的線程將陷入死結;另一個是饑餓(starvation)問題,一個線程由於其他線程總是優先於它而被無限期拖延。
線程互斥和臨界區管理
線程互斥是解決線程間競爭關係的手段。線程互斥(mutual exclusion)是指若干個線程要使用同一共用資源時,任何時刻最多允許一個線程去使用,其他要使用該資源的線程必須等待,直到佔有資源的線程釋放該資源。
共用變數代表的資源成為臨界資源(critical resource),並發線程中與共用變數有關的程式段成為臨界區(critical section)。由於與同一變數有關的臨界區分散在各有關線程的程式段中,而各線程的執行速度不可預知,因此,作業系統對共用一個變數的若干線程各自進入臨界區有以下3個調度原則:
①一次至多一個線程能夠在它的臨界區內
②不能讓一個線程無限期的停留在它的臨界區內
③不能強迫一個線程無限地等待進入它的臨界區。特別地,進入臨界區的任一線程不能妨礙等待進入的其他線程的進展。
把臨界區的調度原則總結成四句話:無空等待、有空讓進、擇一而入、演算法可行。
作業系統提供“互斥鎖”機制實現並發線程互斥地進入臨界區,對共用資源進行操作。
Java的互斥機制實現
Java提供關鍵字synchronized用於聲明一段程式為臨界區,使線程對臨界資源採用互斥使用方式。synchronized有兩種用法:聲明一條語句,或者聲明一個方法。
①同步語句
synchronized(對象)
語句
其中,對象是多個線程共同操作的公開變數,即需要被鎖定的臨界資源。
②同步方法
synchronized 方法聲明
同步方法的方法體成為臨界區,互斥使用(鎖定)的是調用該方法的對象。
線程間的協作關係與線程同步
當合作線程中的一個到達協調點後,在尚未得到其夥伴線程發來的訊號之前應先阻塞自己,直到其他合作線程發來協調訊號後方被喚醒並繼續執行。這種協作線程之間相互等待對方訊息或訊號的協調關係成為線程同步。
線程同步是解決線程間協作關係的手段。線程同步(synchronization)是指兩個以上線程基於某個條件來協調它們的活動。一個線程的執行依賴於另一個線程的訊號,當一個線程沒有得到來自於另一個線程的訊號時則需要等待,直到訊號到達才被喚醒。
線程互斥是一種特殊的線程同步機制,即逐次使用互斥共用資源,也是對線程使用資源次序上的一種協調。
作業系統實現線程同步有一種成為訊號量和PV操作。測試訊號量狀態的操作成為P操作,改變訊號量狀態的操作稱為V操作,這兩種操作是互斥的,並且執行時不能被打斷。多個線程之間彼此根據訊號量的狀態確定誰該執行。
Java的線程通訊方法
java.lang.Object類提供wait()、notify()、和notifyAll()方法實現線程間通訊。
注意:
- 無論synchronized關鍵字加在方法上還是對象上,它取得的鎖都是對象,而不是把一段代碼或函數當作鎖—-而且同步方法很可能還會被其他線程的對象訪問。
- 每個對象只有一個鎖(lock)與之相關聯。
- 實現同步是要很大的系統開銷作為代價的,甚至可能造成死結,所以盡量避免無謂的同步控制。
synchronized作為函數修飾符時,它鎖定的是調用這個同步方法對象。也就是說,當一個對象P1在不同的線程中執行這個同步方法時,它們之間會形成互斥,達到同步的效果。但是這個對象所屬的Class所產生的另一對象P2卻可以任意調用這個被加了synchronized關鍵字的方法。