在我們開發的過程中常常會碰到多線程的問題,對於多線程的實現方式主要有兩種:實現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();