多線程編程優點
-
進程之間不能共享內存,但線程之間共享內存非常容易。
-
系統創建線程所分配的資源相對創建進程而言,代價非常小。
Java中實現多線程有3種方法:
-
繼承Thread類
-
實現Runnable接口
-
實現Callable接口(參考<Java編程思想(第4版)> 21.2.4章節,原來一直以為是2種,后來發現是3種)
第一種實現方法—繼承Thread類
繼承Thread類,需要覆蓋方法 run()方法,在創建Thread類的子類時需要重寫 run(),加入線程所要執行的代即可。
下邊是一個賣票程序小例子:
1 package ThreadOne; 2 3 public class ThreadByExtends { 4 5 public static void main(String[] args) { 6 // TODO Auto-generated method stub 7 new MyThread().start(); 8 new MyThread().start(); 9 new MyThread().start(); 10 } 11 12 } 13 14 class MyThread extends Thread { 15 private int ticket = 5; 16 17 public void run() { 18 19 for (int i = 0; i < 10; i++) { 20 if (ticket > 0) { 21 System.out.println("車票第" + ticket-- + "張"); 22 } 23 } 24 } 25 26 }
輸出結果為:
這樣代碼的寫法簡單,符合大家的習慣,但是直接繼承Thread類有一個很大的缺點,因為“java類的繼承是單一的,extends后面只能指定一個父類”,所有如果當前類繼承Thread類之后就不可以繼承其他類。如果我們的類已經從一個類繼承(如Swing繼承自 Panle 類、JFram類等),則無法再繼承 Thread 類,這時如果我們又不想建立一個新的類,應該怎么辦呢?
第二種實現方法—實現Runnable接口
如果要實現多繼承就得要用implements,Java 提供了接口 java.lang.Runnable 來解決上邊的問題。
Runnable是可以共享數據的,多個Thread可以同時加載一個Runnable,當各自Thread獲得CPU時間片的時候開始運行Runnable,Runnable里面的資源是被共享的,所以使用Runnable更加的靈活。
下邊還是賣票例子:
1 package ThreadOne; 2 3 public class ThreadRunnable { 4 5 public static void main(String[] args) { 6 MyThread1 myThread = new MyThread1(); 7 new Thread(myThread).start(); 8 new Thread(myThread).start(); 9 } 10 } 11 12 class MyThread1 implements Runnable { 13 14 private int ticket = 5; 15 16 public void run() { 17 for (int i = 0; i < 10; i++) { 18 if (ticket > 0) { 19 System.out.println("ticket = " + ticket--); 20 } 21 } 22 } 23 24 }
輸出結果:
- 在第二種方法(Runnable)中,ticket輸出的順序並不是54321,這是因為線程執行的時機難以預測,ticket--並不是原子操作(關於原子操作后邊會有詳解)。
- 在第一種方法中,我們new了3個Thread對象,即三個線程分別執行三個對象中的代碼,因此便是三個線程去獨立地完成賣票的任務;而在第二種方法中,我們同樣也new了3個Thread對象,但只有一個Runnable對象,3個Thread對象共享這個Runnable對象中的代碼,因此,便會出現3個線程共同完成賣票任務的結果。如果我們new出3個Runnable對象,作為參數分別傳入3個Thread對象中,那么3個線程便會獨立執行各自Runnable對象中的代碼,即3個線程各自賣5張票。
- 在第二種方法中,由於3個Thread對象共同執行一個Runnable對象中的代碼,因此可能會造成線程的不安全,比如可能ticket會輸出-1(如果我們System.out....語句前加上線程休眠操作,該情況將很有可能出現),這種情況的出現是由於,一個線程在判斷ticket為1>0后,還沒有來得及減1,另一個線程已經將ticket減1,變為了0,那么接下來之前的線程再將ticket減1,便得到了-1。這就需要加入同步操作(即互斥鎖),確保同一時刻只有一個線程在執行每次for循環中的操作。而在第一種方法中,並不需要加入同步操作,因為每個線程執行自己Thread對象中的代碼,不存在多個線程共同執行同一個方法的情況。
第三種—實現Callable接口
Runnable是執行工作的獨立任務,但是它不返回任何值。如果你希望任務在完成的能返回一個值,那么可以實現Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一種具有類型參數的泛型,它的參數類型表示的是從方法call()(不是run())中返回的值。
例子如下:
1 package ThreadOne; 2 3 import java.awt.Panel; 4 import java.util.concurrent.Callable; 5 import java.util.concurrent.Future; 6 import java.util.concurrent.FutureTask; 7 8 public class ThreadCallable extends Panel { 9 10 public static void main(String[] args) { 11 12 MyThread2 myThread2 = new MyThread2(); 13 14 FutureTask<Integer> futureTask = new FutureTask<>(myThread2); 15 new Thread(futureTask, "線程名:有返回值的線程2").start(); 16 17 try { 18 System.out.println("子線程的返回值:" + futureTask.get()); 19 } catch (Exception e) { 20 e.printStackTrace(); 21 } 22 } 23 } 24 25 class MyThread2 implements Callable<Integer> { 26 27 public Integer call() throws Exception { 28 System.out.println("當前線程名——" + Thread.currentThread().getName()); 29 int i = 0; 30 for (; i < 5; i++) { 31 System.out.println("循環變量i的值:" + i); 32 } 33 34 return i; 35 } 36 37 }
運行結果如下:
總結
實現Runnable接口相比繼承Thread類有如下優勢:
- 可以避免由於Java的單繼承特性而帶來的局限;
- 增強程序的健壯性,代碼能夠被多個線程共享,代碼與數據是獨立的;
- 適合多個相同程序代碼的線程區處理同一資源的情況。
實現Runnable接口和實現Callable接口的區別:
- Runnable是自從java1.1就有了,而Callable是1.5之后才加上去的
- Callable規定的方法是call(),Runnable規定的方法是run()
- Callable的任務執行后可返回值,而Runnable的任務是不能返回值(是void)
- call方法可以拋出異常,run方法不可以
- 運行Callable任務可以拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。通過Future對象可以了解任務執行情況,可取消任務的執行,還可獲取執行結果。
- 加入線程池運行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。