執行緒與程式
什麼是程式?
當一個程式進入記憶體中執行起來它就變為一個程式。因此,程式就是一個處於執行狀態的程式。同時程式具有獨立功能,程式是作業系統進行資源分配和排程的獨立單位。
什麼是執行緒?
執行緒是程式的組成部分。通常情況下,一個程式可擁有多個執行緒,而一個執行緒只能擁有一個父程式。
執行緒可以擁有自己的堆疊、自己的程式計數器及自己的區域性變數,但是執行緒不能擁有系統資源,它與其父程式的其他執行緒共享程式中的全部資源,這其中包括程式的程式碼段、資料段、堆空間以及一些程式級的資源(例如,開啟的檔案等)。
執行緒是程式的執行單元,是CPU排程和分派的基本單位,當程式被初始化之後,主執行緒就會被建立。同時如果有需要,還可以在程式執行過程中建立出其他執行緒,這些執行緒之間也是相互獨立的,並且在同一程式中併發執行。因此一個程式中可以包含多個執行緒,但是至少要包含一個執行緒,即主執行緒。
一個程式中的執行緒
Java中的執行緒
Java 中使用Thread類表示一個執行緒。所有的執行緒物件都必須是Thread或其子類的物件。Thread 類中的 run 方法是該執行緒的執行程式碼。讓我們來看一個例項:
public class Ticket extends Thread{
// 重寫run方法
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(getName() + ": " + i);
}
}
}
public class TestThread {
public static void main(String[] args) {
// 1.建立執行緒
Thread thread1 = new Ticket();
Thread thread2 = new Ticket();
// 2.啟動執行緒
thread1.start();
thread2.start();
}
}
執行結果如下:
通過上面的程式碼和執行結果,我們可以得到:
執行緒執行的幾個特點:
1.同一程式下不同執行緒的排程不由程式控制。執行緒的執行是搶佔式的,執行的順序和執行緒的啟動順序是無關的,當前執行的執行緒隨時都可能被掛起,然後其他程式搶佔執行。
2.執行緒獨享自己的堆疊程式計數器和區域性變數。兩個程式的區域性變數互不干擾,各自的執行順序也是互不干擾。
3.兩個執行緒併發執行。兩個執行緒同時向前推進,並沒有說執行完一個後再執行另一個。
start()方法和run()方法:
啟動一個執行緒必須呼叫Thread 類的 start()方法,使該執行緒處於就緒狀態,這樣該執行緒就可以被處理器排程。
run()方法是一個執行緒所關聯的執行程式碼,無論是派生自 Thread類的執行緒類,還是實現Runnable介面的類,都必須實現run()方法,run()方法裡是我們需要執行緒所執行的程式碼。
實現多執行緒必須呼叫Thread 類的 start()方法來啟動執行緒,使執行緒處於就緒狀態隨時供CPU排程。如果直接呼叫run()方法的話,只是呼叫了Thread類的一個普通方法,會立即執行該方法中的程式碼,並沒有實現多執行緒技術。
Java中多執行緒的實現方法
在Java中有三種方法實現多執行緒。
第一種方法:使用Thread類或者使用一個派生自Thread 類的類構建一個執行緒。
第二種方法:實現Runnable 介面來構建一個執行緒。(推薦使用)
第三種方法:實現Callable 介面來構建一個執行緒。(有返回值)
第一種方法
使用Thread類或者使用一個派生自Thread 類的類構建一個執行緒。
public class Ticket extends Thread{
// 重寫run方法
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(getName() + ": " + i);
}
}
}
public class TestThread {
public static void main(String[] args) {
// 1.建立執行緒
Thread thread1 = new Ticket();
Thread thread2 = new Ticket();
// 2.啟動執行緒
thread1.start();
thread2.start();
}
}
看上面的程式碼,我們建立了一個Ticket類,它繼承了Thread類,重寫了Thread類的run方法。然後我們用Ticket類建立了兩個執行緒,並且啟動了它們。但我們不推薦使用這種方法,因為一個類繼承了Thread類,那它就沒有辦法繼承其他類了,這對較為複雜的程式開發是不利的。
第二種方法
實現Runnable 介面來構建一個執行緒。
public class Ticket implements Runnable{
// 重寫run方法
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
public class TestThread {
public static void main(String[] args) {
// 1.建立執行緒
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Thread thread1 = new Thread(t1, "買票1號");
Thread thread2 = new Thread(t2, "買票2號");
// 2.啟動執行緒
thread1.start();
thread2.start();
}
}
我們建立了一個Ticket類,實現了Runnable介面,在該類中實現了run方法。在啟動執行緒前,我們要建立一個執行緒物件,不同的是我們要將一個實現了Runnable介面的類的物件作為Thread類構造方法的引數傳入,以構建執行緒物件。構造方法Thread的第二個引數用來指定該執行緒的名字,通過Thread.currentThread().getName()可獲取當前執行緒的名字。
在真實的專案開發中,推薦使用實現Runnable介面的方法進行多執行緒程式設計。因為這樣既可以實現一個執行緒的功能,又可以更好地複用其他類的屬性和方法。
第三種方法
實現Callable 介面來構建一個執行緒。
public class TestThread {
public static void main(String[] args) {
// 1.建立Callable的例項
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(7000);
return "我結束了";
}
};
// 2.通過FutureTask介面的例項包裝Callable的例項
FutureTask<String> futureTask = new FutureTask<String>(callable);
// 3.建立執行緒並啟動
new Thread(futureTask).start();
// 4.獲得結果並列印
try {
System.out.println(futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
首先我們用匿名內部類建立了一個實現Callable介面的類的物件,然後通過FutureTask 的例項包裝了Callable的例項,這樣我們就可以通過一個Thread 物件在新執行緒中執行call()方法,同時又可以通過get方法獲取到call()的返回值。然後建立執行緒並啟動它,最後線上程執行完執行完call()方法後得到返回值並列印。
我們來看一下Callable的原始碼:
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
從Callable 的定義可以看出,Callable介面是一個泛型介面,它定義的call()方法類似於Runnable 的run()方法,是執行緒所關聯的執行程式碼。但是與run()方法不同的是,call()方法具有返回值,並且泛型介面的引數V指定了call()方法的返回值型別。同時,如果call()方法得不到返回值將會丟擲一個異常,而在Runnable的run()方法中不能丟擲異常。
如何獲得call()方法的返回值呢?
通過Future介面來獲取。Future介面定義了一組對 Runnable 或者Callable 任務的執行結果進行取消、查詢、獲取、設定的操作。其中get方法用於獲取call()的返回值,它會發生阻塞,直到call()返回結果。
這樣的執行緒呼叫與直接同步呼叫函式有什麼差異呢?
在上面的例子中,通過future.get()獲取 call()的返回值時,由於call方法中會 sleep 7s,所以在執行future.get()的時候主執行緒會被阻塞而什麼都不做,等待call()執行完並得到返回值。但是這與直接呼叫函式獲取返回值還是有本質區別的。
因為call()方法是執行在其他執行緒裡的,在這個過程中主執行緒並沒有被阻塞,還是可以做其他事情的,除非執行future.get()去獲取 call()的返回值時主執行緒才會被阻塞。所以當呼叫了Thread.start()方法啟動 Callable 執行緒後主執行緒可以執行別的工作,當需要call()的返回值時再去呼叫future.get()獲取,此時call()方法可能早已執行完畢,這樣就可以既確保耗時操作在工作執行緒中完成而不阻擋主執行緒,又可以得到執行緒執行結果的返回值。而直接呼叫函式獲取返回值是一個同步操作,該函式本身就是執行在主執行緒中,所以一旦函式中有耗時操作,必然會阻擋主執行緒。