淺談 Java多線程
線程與進程
什麼是進程?
當一個程序進入內存中運行起來它就變為一個進程。因此,進程就是一個處於運行狀態的程序。同時進程具有獨立功能,進程是操作系統進行資源分配和調度的獨立單位。
什麼是線程?
線程是進程的組成部分。通常情況下,一個進程可擁有多個線程,而一個線程只能擁有一個父進程。
線程可以擁有自己的堆棧、自己的程序計數器及自己的局部變量,但是線程不能擁有系統資源,它與其父進程的其他線程共享進程中的全部資源,這其中包括進程的代碼段、數據段、堆空間以及一些進程級的資源(例如,打開的文件等)。
線程是進程的執行單元,是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()方法可能早已執行完畢,這樣就可以既確保耗時操作在工作線程中完成而不阻擋主線程,又可以得到線程執行結果的返回值。而直接調用函數獲取返回值是一個同步操作,該函數本身就是運行在主線程中,所以一旦函數中有耗時操作,必然會阻擋主線程。