JAVA多執行緒之Runnable和Thread比較
在我們開發的過程中常常會碰到多執行緒的問題,對於多執行緒的實現方式主要有兩種:實現Runnable介面、繼承Thread類。對於這兩種多執行緒的實現方式也是有著一些差異。既然實現了多執行緒那必然離不開管理這些執行緒,當問題比簡單時一個或者幾個執行緒就OK了,也涉及不到效率問題。一旦執行緒數量多起來的時候,必然躲不過這些執行緒的建立與銷燬,而往往這是很浪費時間的。這時就需要利用執行緒池來進行管理,既免去了我們建立執行緒和銷燬執行緒的程式碼,也提高了程式的效率。下面針對以上問題做出相關講解。
一、Runnable、Thread比較
首先闡述實現Runnable的好處:
- java不允許多繼承,因此實現了Runnable介面的類可以再繼承其他類。
- 方便資源共享,即可以共享一個物件例項???(從很多部落格中看到這樣描述,但是此處有疑問,例子如下)
下面來通過具體程式碼來解釋上述優點,網上很流行的買票系統,假設有10張票,首先通Thread來進行購買。程式碼如下:
public class TicketThread extends Thread{
private int ticket = 10;
public void run(){
for(int i =0;i<10;i++){
synchronized (this){
if(this.ticket>0 ){
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"賣票---->"+(this.ticket--));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] arg){
TicketThread t1 = new TicketThread();
new Thread(t1,"執行緒1").start();
new Thread(t1,"執行緒2").start();
//也達到了資源共享的目的,此處網上有各種寫法,很多寫法都是自圓其說,舉一些特殊例子來印證自己的觀點,然而事實卻不盡如此。
}
}
輸出:
執行緒1賣票—->10
執行緒1賣票—->9
執行緒1賣票—->8
執行緒2賣票—->7
執行緒2賣票—->6
執行緒1賣票—->5
執行緒1賣票—->4
執行緒2賣票—->3
執行緒2賣票—->2
執行緒1賣票—->1
實現Runnable介面:
package threadTest;
public class TicketRunnable implements Runnable{
private int ticket = 10;
@Override
public void run() {
for(int i =0;i<10;i++){
//新增同步快
synchronized (this){
if(this.ticket>0){
try {
//通過睡眠執行緒來模擬出最後一張票的搶票場景
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"賣票---->"+(this.ticket--));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] arg){
TicketRunnable t1 = new TicketRunnable();
new Thread(t1, "執行緒1").start();
new Thread(t1, "執行緒2").start();
}
}
輸出:
執行緒1賣票—->10
執行緒1賣票—->9
執行緒1賣票—->8
執行緒1賣票—->7
執行緒2賣票—->6
執行緒2賣票—->5
執行緒2賣票—->4
執行緒2賣票—->3
執行緒2賣票—->2
執行緒2賣票—->1
從這兩個例子可以看出,Thread也可以資源共享啊,為什麼呢,因為Thread本來就是實現了Runnable,包含Runnable的功能是很正常的啊!!至於兩者的真正區別最主要的就是一個是繼承,一個是實現;其他還有一些面向物件的思想,Runnable就相當於一個作業,而Thread才是真正的處理執行緒,我們需要的只是定義這個作業,然後將作業交給執行緒去處理,這樣就達到了鬆耦合,也符合面向物件裡面組合的使用,另外也節省了函式開銷,繼承Thread的同時,不僅擁有了作業的方法run(),還繼承了其他所有的方法。綜合來看,用Runnable比Thread好的多。
針對本例再補充一點,在以上程式中,如果去掉同步程式碼塊,則會出現其中一人購買第0張票的情況,所以我們在做多執行緒並行的時候一定要時刻考慮到邊界值的問題,在關鍵程式碼處必須要做好同步處理。
二、執行緒池
建立執行緒池主要有三個靜態方法供我們使用,由Executors來進行建立相應的執行緒池:
public static ExecutorSevice newSingleThreadExecutor()
public static ExecutorSevice newFixedThreadPool(int nThreads)
public static ExecutorSevice newCachedThreadPool()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
- newSingleThreadExecutor返回以個包含單執行緒的Executor,將多個任務交給此Exector時,這個執行緒處理完一個任務後接著處理下一個任務,若該執行緒出現異常,將會有一個新的執行緒來替代。
- newFixedThreadPool返回一個包含指定數目執行緒的執行緒池,如果任務數量多於執行緒數量,那麼沒有執行的任務必須等待,直到有任務完成為止。
- newCachedThreadPool根據使用者的任務數建立相應的執行緒來處理,該執行緒池不會對執行緒數目加以限制,完全依賴於JVM能建立執行緒的數量,可能引起記憶體不足。
- newScheduledThreadPool建立一個至少有n個執行緒空間大小的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。
我們只需要把實現了Runnable的類的物件例項放入執行緒池,那麼執行緒池就自動維護執行緒的啟動、執行、銷燬。我們不需要自行呼叫start()方法來開啟這個執行緒。執行緒放入執行緒池之後會處於等待狀態直到有足夠空間時會喚醒這個執行緒。
private ExecutorService threadPool = Executors.newFixedThreadPool(5);
threadPool.execute(socketThread);
//至少維護5個執行緒容量的空間
private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
//函式意義:一個執行緒開始之後和下一個執行緒開始的時間間隔
//第一個時間引數表示初始化執行延遲1000毫秒,第二個時間引數表示每隔1000毫秒執行一次
//第二個執行緒必須等到第一個執行緒執行完成才能繼續執行,儘管時間間隔小於執行緒執行時間
threadPool.scheduleAtFixedRate(socketThread, 1000, 1000, TimeUnit.MILLISECONDS);
//基本引數和上面的類似,函式意義不一樣:一個執行緒結束之後和下一個執行緒開始的時間間隔
threadPool.scheduleWithFixedDelay(socketThread, 1000, 1000, TimeUnit.MILLISECONDS);
//執行緒池不接收新加的執行緒,但是執行完執行緒池內部的所有執行緒
threadPool.shutdown();
//立即關閉執行緒池,停止執行緒池內還未執行的執行緒並且返回一個未執行的執行緒池列表
threadPool.shutdownNow();