標籤:
Java多線程之 – 進程和線程概念進程
- 程式的動態執行過程
- 包括佔用的資源(記憶體、CPU)和線程
線程
- 線程是程式中最小的執行單位
- 一個進程有多個線程
- 線程共用進程的資源
進程和線程的區分
我們可以想象為進程為班級而線程是邦奇中得每一個學生
線程之間的互動
- 互斥,類似於每一個學生都為了第一名而你爭我讓,線程也是,都想搶佔CPU的資源
- 同步,當舉行運動會的時候,大家都團結一心,彼此共用自己的資源
Thread、RunnableThreadIntroduction
Thread 是Java當中的線程,線程是一個程式的多個執行路徑,執行調度的單位,依託於進程存在。 線程不僅可以共用進程的記憶體,而且還擁有一個屬於自己的記憶體空間,這段記憶體空間也叫做線程棧,是在建立線程時由系統分配的,主要用來儲存線程內部所使用的資料,如線程執行函數中所定義的變數。注意:Java中的多線程是一種搶佔機制而不是分時機制。搶佔機制指的是有多個線程處於可運行狀態,但是只允許一個線程在運行,他們通過競爭的方式搶佔CPU。
Define
/** * 使用繼承java.lang.Thread類的方式建立一個線程 * * @author DreamSea 2011-12-29 20:17:06 */public class ThreadTest extends Thread { /** * 重寫(Override)run()方法 JVM會自動調用該方法 */ public void run() { System.out.println("I‘m running!"); }}
注意,重寫(override)run()方法在該線程的start()方法之後被調用,但是重載(overload)run()方法,該方法和普通的成員方法一樣,並不會因調用該線程的start()方法而被JVM自動運行。
public class ThreadTest extends Thread { /** * 重寫(Override)run()方法 JVM會自動調用該方法 */ @Override public void run() { System.out.println("I‘m running!"); } /** * 重載(Overload)run()方法 和普通的方法一樣,並不會在該線程的start()方法被調用後被JVM自動運行 */ public void run(int times) { System.out.println("I‘m running!(Overload)"); }}
使用建議
不建議使用這種方法,因為這個類繼承自Thread之後不能再去繼承其他的類,而且這種繼承實現的方法是和實現Runnable介面有區別的
Start
ThreadTest thread = new ThreadTest();thread.start();
State
新生狀態(New): 當一個線程的執行個體被建立即使用new關鍵字和Thread類或其子類建立一個線程對象後,此時該線程處於新生(new)狀態,處於新生狀態的線程有自己的記憶體空間,但該線程並沒有運行,此時線程還不是活著的(not alive);
就緒狀態(Runnable): 通過調用線程執行個體的start()方法來啟動線程使線程進入就緒狀態(runnable);處於就緒狀態的線程已經具備了運行條件,但還沒有被分配到CPU即不一定會被立即執行,此時處於線程就緒隊列,等待系統為其分配CPU,等待狀態並不是執行狀態;此時線程是活著的(alive);
阻塞狀態(Blocked): 通過調用join()、sleep()、wait()或者資源被暫用使線程處於阻塞(blocked)狀態;處於Blocking狀態的線程仍然是活著的(alive);
死亡狀態(Dead): 當一個線程的run()方法運行完畢或被中斷或被異常退出,該線程到達死亡(dead)狀態。此時可能仍然存在一個該Thread的執行個體對象,當該Thread已經不可能在被作為一個可被獨立執行的線程對待了,線程的獨立的call stack已經被dissolved。一旦某一線程進入Dead狀態,他就再也不能進入一個獨立線程的生命週期了。對於一個處於Dead狀態的線程調用start()方法,會出現一個運行期(runtime exception)的異常;處於Dead狀態的線程不是活著的(not alive).
線程生命週期: new 一個對象然後經過thread.start()執行Runnable,在JVM調度獲得CPU資源之後處於running狀態,最後死亡,中間線程可能被阻塞或者沒有被調度。
Context
對於單核CPU來說(對於多核CPU,此處就理解為一個核),CPU在一個時刻只能運行一個線程,當在運行一個線程的過程中轉去運行另外一個線程,這個叫做線程環境切換(對於進程也是類似)。
由於可能當前線程的任務並沒有執行完畢,所以在切換時需要儲存線程的運行狀態,以便下次重新切換回來時能夠繼續切換之前的狀態運行。舉個簡單的例子:比如一個線程A正在讀取一個檔案的內容,正讀到檔案的一半,此時需要暫停線程A,轉去執行線程B,當再次切換回來執行線程A的時候,我們不希望線程A又從檔案的開頭來讀取。
因此需要記錄線程A的運行狀態,那麼會記錄哪些資料呢?因為下次恢複時需要知道在這之前當前線程已經執行到哪條指令了,所以需要記錄程式計數器的值,另外比如說線程進行中某個計算的時候被掛起了,那麼下次繼續執行的時候需要知道之前掛起時變數的值時多少,因此需要記錄CPU寄存器的狀態。所以一般來說,線程環境切換過程中會記錄程式計數器、CPU寄存器狀態等資料。
說簡單點的:對於線程的環境切換實際上就是 儲存和恢複CPU狀態的過程,它使得線程執行能夠從中斷點恢複執行。
雖然多線程可以使得任務執行的效率得到提升,但是由於線上程切換時同樣會帶來一定的開銷代價,並且多個線程會導致系統資源佔用的增加,所以在進行多線程編程時要注意這些因素。
Function
| 類別 |
方法簽名 |
簡介 |
| 線程的建立 |
Thread() |
|
| 線程的建立 |
Thread(String name) |
|
| 線程的建立 |
Thread(Runnable runnable) |
|
| 線程的建立 |
Thread(Runnable target, String name) |
|
| 線程的方法 |
start() |
啟動線程 |
| 線程的方法 |
static void sleep(long millis) |
休眠線程 |
| 線程的方法 |
static void sleep(long millis, int nanos) |
休眠線程 |
| 線程的方法 |
void join() |
使其他線程等待當前線程終止 |
| 線程的方法 |
void join(long millis) |
使其他線程等待當前線程終止 |
| 線程的方法 |
void join(long millis, int nanos) |
使其他線程等待當前線程終止 |
| 擷取可用線程 |
static Thread currentThread() |
返回當前啟動並執行線程的引用 |
注意:
- sleep相當於讓線程睡眠,交出CPU,讓CPU去執行其他的任務。
但是有一點要非常注意,sleep方法不會釋放鎖,也就是說如果當前線程持有對某個對象的鎖,則即使調用sleep方法,其他線程也無法訪問這個對象。
public class Test implements Runnable { @Override public void run(){ this.s(); } public synchronized void s(){ System.out.println(Thread.currentThread().getName() + " get lock."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " release lock."); } public static void main(String[] args){ Test st = new Test(); Thread t1 = new Thread(st, "t1"); Thread t2 = new Thread(st, "t2"); t1.start(); t2.start(); }}
他的輸出資料為:
t1 get lock.
t1 release lock.
t2 get lock.
t2 release lock.
yield方法調用yield方法會讓當前線程交出CPU許可權,讓CPU去執行其他的線程。它跟sleep方法類似,同樣不會釋放鎖。但是yield不能控制具體的交出CPU的時間,另外,yield方法只能讓擁有相同優先順序的線程有擷取CPU執行時間的機會。
wait方法會讓線程進入阻塞狀態,並且會釋放線程佔有的鎖,並交出CPU執行許可權。
由於wait方法會讓線程釋放對象鎖,所以join方法同樣會讓線程釋放對一個對象持有的鎖。具體的wait方法使用在後面文章中給出。
public class Test1 implements Runnable { private Object lock; public Test1(Object lock){ this.lock = lock; } @Override public void run(){ synchronized(lock){ System.out.println(Thread.currentThread().getName() + " get lock."); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " release lock."); } }}
public class Test2 implements Runnable { private Object lock; public Test2(Object lock){ this.lock = lock; } @Override public void run() { try { Thread.sleep(2*1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(lock){ System.out.println(Thread.currentThread().getName() + " get lock."); lock.notifyAll(); System.out.println(Thread.currentThread().getName() + " release lock."); } synchronized(lock){ System.out.println(Thread.currentThread().getName() + " get lock."); System.out.println(Thread.currentThread().getName() + " release lock."); } } public static void main(String[] args){ Object lock = new Object(); Thread t1 = new Thread(new Test1(lock), "t1"); Thread t2 = new Thread(new Test2(lock), "t2"); t1.start(); t2.start(); }}
結果如下:
t1 get lock.
t2 get lock.
t2 release lock.
t2 get lock.
t2 release lock.
t1 release lock.
- interrupt方法interrupt,顧名思義,即中斷的意思。單獨調用interrupt方法可以使得處於阻塞狀態的線程拋出一個異常,也就說,它可以用來中斷一個正處於阻塞狀態的線程;另外,通過interrupt方法和isInterrupted()方法來停止正在啟動並執行線程。
RunnableDefinie
一個類如果需要具備多線程的能力,也可以通過實現java.lang.Runnable介面進行實現。按照Java語言的文法,一個類可以實現任意多個介面,所以該種實現方式在實際實現時的通用性要比前面介紹的方式好一些。但是單獨的run方法是不能非同步執行的,也就是直接調用run方法與Thread調用run方法結果會不一樣。
差異性
以買票程式為例子,當我們使用Runnable實現多線程時
public class RunnableTest implements Runnable{ public static int count = 10; public String name; public RunnableTest(String name){ this.name = name; } @Override public void run() { while(count > 0){ count--; print("線程: " + name + ",現在還有 " + count +" 張票!"); } } public static void main(String args[]){ RunnableTest test1 = new RunnableTest("小紅"); RunnableTest test2 = new RunnableTest("小綠"); RunnableTest test3 = new RunnableTest("小藍"); new Thread(test1).start(); new Thread(test2).start(); new Thread(test3).start(); } public static void print(Object o){ System.out.println(o.toString()); }}
輸出結果是:
線程: 小紅,現在還有 9 張票!
線程: 小紅,現在還有 6 張票!
線程: 小藍,現在還有 7 張票!
線程: 小藍,現在還有 4 張票!
線程: 小綠,現在還有 8 張票!
線程: 小藍,現在還有 3 張票!
線程: 小紅,現在還有 5 張票!
線程: 小藍,現在還有 1 張票!
線程: 小綠,現在還有 2 張票!
線程: 小紅,現在還有 0 張票!
然而當我們使用Thread的時候結果不一樣了:
線程: 小紅,現在還有 8 張票!
線程: 小藍,現在還有 7 張票!
線程: 小綠,現在還有 7 張票!
線程: 小藍,現在還有 5 張票!
線程: 小紅,現在還有 6 張票!
線程: 小藍,現在還有 3 張票!
線程: 小綠,現在還有 4 張票!
線程: 小藍,現在還有 1 張票!
線程: 小紅,現在還有 2 張票!
線程: 小綠,現在還有 0 張票!
可以看出出現了重複的資料,這就是Runnable和Thread在不同實現情況之下的區別,這是因為繼承Thread不能保證共用資料的可見度,至於什麼是共用資料,怎麼共用資料,我們下次再說
Java多線程之 -- 進程和線程