在計算機世界,當人們談到並發時,它的意思是一系列的任務在計算機中同時執行。如果計算機有多個處理器或者多核處理器,那么這個同時性是真實發生的;如果計算機只有一個核心處理器那么就只是表面現象。
現代所有的操作系統都允許並發地執行任務。你可以在聽音樂和瀏覽網頁新聞的同時閱讀郵件,我們說這種並發是進程級別的並發。而且在同一進程內,也會同時有多種任務,這些在同一進程內運行的並發任務稱之為線程。
在這里我們要討論的是線程級並發。Java提供了Thread類,使我們能夠在一個Java進程中運行多個線程,每個線程執行不同的任務,以此實現並發。
1、創建線程
在Java中,我們有2個方式創建線程:
- 通過直接繼承thread類,然后覆蓋run()方法。
- 構建一個實現Runnable接口的類, 然后創建一個thread類對象並傳遞Runnable對象作為構造參數
可以看到,這兩種創建線程的方式都需要新建一個Thread對象,可以說一個Thread對象代表一個線程實例。
由於Java是單繼承的,如果我們使用第一種方式創建線程,就強制只能繼承Thread,靈活性較低,對於第二種創建線程方式就沒有這個問題,所以我們一般選擇第二種方式創建線程。下面我們就使用第二種創建方式舉一個簡單的例子,首先,實現Runnable接口,輸出1到10:
public class MyRunnable implements Runnable { @Override public void run() { for(int i = 0; i < 10; i++) { System.out.println(i); } } }
然后,建立main方法,新建線程並啟動:
public class Main { public static void main(String[] args) { for(int i = 0; i < 10; i++) { new Thread(new MyRunnable()).start(); } } }
可以看到使用這種方法創建線程,我們一共創建了兩個類,一個類實現了Runnable接口,代表一個運行任務;一個類是運行類,在這個運行類的main方法中,我們利用剛剛定義的Runnable實例新建Thread並運行。利用這種方法創建線程,相較於第一種,代碼邏輯十分清楚:一個類定義任務,一個類運行任務,所以推薦這種方式創建線程。
2、運行線程
運行一個線程的方法十分簡單,我們只需調用Thread實例的start()方法即可,當我們調用start()方法之后,Java虛擬機會為我們創建一個線程,然后在該線程中運行run()方法中定義的任務,真正實現多線程。
在這里,必須強調的是應該調用start()方法,而不是run()方法,直接調用run()方法,虛擬機不會新建線程運行任務,只會在當前線程執行任務,無法實現多線程並發,所以應該調用start()方法。
3、停止線程
相較於創建與運行線程的簡單,在Java中停止一個線程其實並不容易。Thread類雖然提供了stop()方法用於停止線程,但是該方法具有固有的不安全性。用 Thread.stop 來終止線程將釋放它已經鎖定的所有監視器。如果以前受這些監視器保護的任何對象處於一種不一致的狀態,則損壞的對象將對其他線程可見,這有可能導致任意的行為,該方法已經被標注為過時的了,我們不應該使用該方法。
3.1、"已請求取消"標志
既然如此,那么我們應該如何停止一個線程呢?我們可以使用"已請求取消"標志的方法停止線程。我們先在任務中定義該標志,然后任務會定期的查看該標志,如果設置了這個標志,那么任務將提前結束。以下程序使用該項技術來持續枚舉素數,直到它被取消,注意,為保證這個過程能可靠地工作,標志必須設置為volatile類型:
public class PrimeGenerator implements Runnable { private final List<BigInteger> primes = new ArrayList<BigInteger>(); private volatile boolean cancelled; public void run() { BigInteger p = BigInteger.ONE; while (!cancelled) { p = p.nextProbablePrime(); synchronized (this) { primes.add(p); } } } public void cancel() { cancelled = true; } public synchronized List<BigInteger> get() { return new ArrayList<BigInteger>(primes); } }
在這里,PrimeGenerator類提供了cancel()方法,當這個方法被調用后,cancelled標志位就會被置為true,然后run()方法中的while循環就會結束,整個線程也就結束。
3.2、使用中斷停止線程
使用"已請求取消"標志取消任務有一個問題,如果線程執行的是阻塞任務,那么線程將永遠不會去檢測取消標志,因此永遠不會結束。
當出現這種情況時,我們該如何結束線程呢?有部分阻塞庫方法是支持中斷的,線程中斷是一種協作機制,線程可以通過這種機制來通知另一個線程,告訴它在合適的或者可能的情況下停止當前工作,並轉而執行其他工作。
我們可以修改上面的例子,不使用ArrayList存儲素數結果,而使用BlockingQueue來存儲,BlockingQueue的put()方法是可阻塞的,如果依然使用"已請求取消"標志的結束策略,同時put()方法被阻塞住后,那么該方法將永遠不會停止,對於這種情況,我們可以使用檢測中斷標志位的方法來判斷結束線程:
public class PrimeProducer extends Thread { private final BlockingQueue<BigInteger> queue; PrimeProducer(BlockingQueue<BigInteger> queue) { this.queue = queue; } public void run() { try { BigInteger p = BigInteger.ONE; while (!Thread.currentThread().isInterrupted()) queue.put(p = p.nextProbablePrime()); } catch (InterruptedException consumed) { /* Allow thread to exit */ } } public void cancel() { interrupt(); } }
這一次,cancle()方法不再設置結束標志位,而是調用interrupt()進行線程中斷。當cancel()方法被調用之后,當前線程的中斷標志位將被置為true,BlockingQueue的put()方法能夠響應中斷,並從阻塞狀態返回,返回后,while語句檢測到中斷位被標志,然后結束while循環,整個線程結束。
3.3、不能響應中斷的阻塞方法
如果阻塞方法能夠響應中斷,那我們就可以使用以上的方法結束線程,但是Java類庫中還有一些阻塞方法是不能夠響應中斷的,這些方法包括:
- java.io包中的同步Socket I/O
- java.io包中的同步I/O
- Selector的異步I/O
- 獲取某個鎖
不過還好,對於I/O流,我們可以使用關閉底層I/O流的方式結束線程,以下代碼給出了如何封裝非標准的取消操作的例子,ReaderThread管理一個套接字連接,它采用同步方式從套接字中讀取數據,為了結束某個用戶的連接或者關閉服務器,ReaderThread改寫了interrupt方法,使其既能處理標准中斷,也能關閉底層套接字。
public class ReaderThread extends Thread { private static final int BUFSZ = 512; private final Socket socket; private final InputStream in; public ReaderThread(Socket socket) throws IOException { this.socket = socket; this.in = socket.getInputStream(); } public void interrupt() { try { socket.close(); } catch (IOException ignored) { } finally { super.interrupt(); } } public void run() { try { byte[] buf = new byte[BUFSZ]; while (true) { int count = in.read(buf); if (count < 0) break; else if (count > 0) processBuffer(buf, count); } } catch (IOException e) { /* Allow thread to exit */ } } public void processBuffer(byte[] buf, int count) { } }
對於這個具體的Thread類來說,我們調用interrupt()方法,線程首先會把socket關閉,然后再finally()中設置中斷標志位。關閉socket之后,run()方法中的socket讀取將立即拋異,catch子句將捕獲該異常並順利停止該線程。
3.4、總結
最后,讓我對上面的內容總結一下:
要結束一個線程,最理想方式是讓其自動結束,如果你想提前結束線程的運行,那么需要區分三種情況。
1、如果允許代碼中不存在阻塞方法,你可以設置一個"結束"標志位,然后不停的檢測它,當它為true時,主動結束線程;
2、如果代碼中存在阻塞方法,且該方法能夠響應中斷,那么你可以調用Thread.interput()結束線程;
3、如果代碼中存在阻塞方法,且該方法不能夠響應中斷,那么就需要通過關閉底層資源,讓代碼拋出異常的方式結束線程。