Java並發(基礎知識)—— 創建、運行以及停止一個線程


      在計算機世界,當人們談到並發時,它的意思是一系列的任務在計算機中同時執行。如果計算機有多個處理器或者多核處理器,那么這個同時性是真實發生的;如果計算機只有一個核心處理器那么就只是表面現象。

      現代所有的操作系統都允許並發地執行任務。你可以在聽音樂和瀏覽網頁新聞的同時閱讀郵件,我們說這種並發是進程級別的並發。而且在同一進程內,也會同時有多種任務,這些在同一進程內運行的並發任務稱之為線程。

      在這里我們要討論的是線程級並發。Java提供了Thread類,使我們能夠在一個Java進程中運行多個線程,每個線程執行不同的任務,以此實現並發。

1、創建線程                                                                                                 

    在Java中,我們有2個方式創建線程:

  1. 通過直接繼承thread類,然后覆蓋run()方法。
  2. 構建一個實現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、如果代碼中存在阻塞方法,且該方法不能夠響應中斷,那么就需要通過關閉底層資源,讓代碼拋出異常的方式結束線程。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM